diff --git a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs index ab70fd39..dcd9a5c2 100644 --- a/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/StatisticsEndpoints.cs @@ -1,4 +1,6 @@ using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.API.Responses; using LBPUnion.ProjectLighthouse.Types.Users; @@ -31,10 +33,10 @@ public class StatisticsEndpoints : ApiEndpointController new StatisticsResponse { Photos = await StatisticsHelper.PhotoCount(this.database), - Slots = await StatisticsHelper.SlotCount(this.database), + Slots = await StatisticsHelper.SlotCount(this.database, new SlotQueryBuilder()), Users = await StatisticsHelper.UserCount(this.database), RecentMatches = await StatisticsHelper.RecentMatches(this.database), - TeamPicks = await StatisticsHelper.TeamPickCount(this.database), + TeamPicks = await StatisticsHelper.SlotCount(this.database, new SlotQueryBuilder().AddFilter(new TeamPickFilter())), } ); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index be54b098..78f9cc51 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -4,6 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; using LBPUnion.ProjectLighthouse.Types.Users; @@ -42,12 +43,10 @@ public class CommentController : ControllerBase [HttpGet("comments/{slotType}/{slotId:int}")] [HttpGet("userComments/{username}")] - public async Task GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, string? slotType, int slotId) + public async Task GetComments(string? username, string? slotType, int slotId) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0 || pageStart < 0) return this.BadRequest(); - if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest(); if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); @@ -55,6 +54,8 @@ public class CommentController : ControllerBase int targetId; CommentType type = username == null ? CommentType.Level : CommentType.Profile; + PaginationData pageData = this.Request.GetPaginationData(); + if (type == CommentType.Level) { targetId = await this.database.Slots.Where(s => s.SlotId == slotId) @@ -82,8 +83,7 @@ public class CommentController : ControllerBase .Where(p => !blockedUsers.Contains(p.PosterUserId)) .Include(c => c.Poster) .Where(p => p.Poster.PermissionLevel != PermissionLevel.Banned) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(c => GameComment.CreateFromEntity(c, token.UserId)); return this.Ok(new CommentListResponse(comments)); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Login/ClientConfigurationController.cs similarity index 99% rename from ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs rename to ProjectLighthouse.Servers.GameServer/Controllers/Login/ClientConfigurationController.cs index d0806072..ccb8176d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Login/ClientConfigurationController.cs @@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Login; [ApiController] [Authorize] diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Login/LoginController.cs similarity index 99% rename from ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs rename to ProjectLighthouse.Servers.GameServer/Controllers/Login/LoginController.cs index 2c797ee7..8e3886a5 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Login/LoginController.cs @@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Login; [ApiController] [Route("LITTLEBIGPLANETPS3_XML/login")] diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LogoutController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Login/LogoutController.cs similarity index 99% rename from ProjectLighthouse.Servers.GameServer/Controllers/LogoutController.cs rename to ProjectLighthouse.Servers.GameServer/Controllers/Login/LogoutController.cs index 5ee0572c..89140f71 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LogoutController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Login/LogoutController.cs @@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Login; [ApiController] [Authorize] diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ReportController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ReportController.cs index 74bb62ee..bf5007c3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ReportController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ReportController.cs @@ -64,5 +64,4 @@ public class ReportController : ControllerBase return this.Ok(); } - } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index 4986025f..b9f51493 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Serialization; @@ -159,54 +160,53 @@ public class PhotosController : ControllerBase } [HttpGet("photos/{slotType}/{id:int}")] - public async Task SlotPhotos([FromQuery] int pageStart, [FromQuery] int pageSize, string slotType, int id) + public async Task SlotPhotos(string slotType, int id) { - if (pageSize <= 0) return this.BadRequest(); if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest(); if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); + PaginationData pageData = this.Request.GetPaginationData(); + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.SlotId == id) .OrderByDescending(s => s.Timestamp) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); } [HttpGet("photos/by")] - public async Task UserPhotosBy([FromQuery] string user, [FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task UserPhotosBy(string user) { - if (pageSize <= 0) return this.BadRequest(); int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); + PaginationData pageData = this.Request.GetPaginationData(); + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.CreatorId == targetUserId) .OrderByDescending(s => s.Timestamp) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); } [HttpGet("photos/with")] - public async Task UserPhotosWith([FromQuery] string user, [FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task UserPhotosWith(string user) { - if (pageSize <= 0) return this.BadRequest(); - int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); + PaginationData pageData = this.Request.GetPaginationData(); + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.PhotoSubjects.Any(ps => ps.UserId == targetUserId)) .OrderByDescending(s => s.Timestamp) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs new file mode 100644 index 00000000..cfc3752f --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs @@ -0,0 +1,169 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Misc; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; + +[ApiController] +[Authorize] +[Route("LITTLEBIGPLANETPS3_XML/")] +[Produces("text/xml")] +public class CategoryController : ControllerBase +{ + private readonly DatabaseContext database; + + public CategoryController(DatabaseContext database) + { + this.database = database; + } + + [HttpGet("searches")] + [HttpGet("genres")] + public async Task GenresAndSearches() + { + GameTokenEntity token = this.GetToken(); + + UserEntity? user = await this.database.UserFromGameToken(token); + if (user == null) return this.Forbid(); + + PaginationData pageData = this.Request.GetPaginationData(); + + pageData.TotalElements = CategoryHelper.Categories.Count; + + if (!int.TryParse(this.Request.Query["num_categories_with_results"], out int results)) results = 5; + + List categories = new(); + + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); + + foreach (Category category in CategoryHelper.Categories.Skip(Math.Max(0, pageData.PageStart - 1)) + .Take(Math.Min(pageData.PageSize, pageData.MaxElements)) + .ToList()) + { + int numResults = results > 0 ? 1 : 0; + categories.Add(await category.Serialize(this.database, token, queryBuilder, numResults)); + results--; + } + + return this.Ok(new CategoryListResponse(categories, pageData.TotalElements, "", pageData.HintStart)); + } + + [HttpGet("searches/{endpointName}")] + public async Task GetCategorySlots(string endpointName) + { + GameTokenEntity token = this.GetToken(); + + UserEntity? user = await this.database.UserFromGameToken(token); + if (user == null) return this.Forbid(); + + Category? category = CategoryHelper.Categories.FirstOrDefault(c => c.Endpoint == endpointName); + if (category == null) return this.NotFound(); + + PaginationData pageData = this.Request.GetPaginationData(); + + Logger.Debug("Found category " + category, LogArea.Category); + + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); + + GenericSerializableList returnList = category switch + { + SlotCategory gc => await this.GetSlotCategory(gc, token, queryBuilder, pageData), + PlaylistCategory pc => await this.GetPlaylistCategory(pc, token, pageData), + UserCategory uc => await this.GetUserCategory(uc, token, pageData), + _ => new GenericSerializableList(), + }; + + return this.Ok(returnList); + } + + private async Task GetUserCategory(UserCategory userCategory, GameTokenEntity token, PaginationData pageData) + { + int totalUsers = await userCategory.GetItems(this.database, token).CountAsync(); + pageData.TotalElements = totalUsers; + IQueryable userQuery = userCategory.GetItems(this.database, token).ApplyPagination(pageData); + + List users = + (await userQuery.ToListAsync()).ToSerializableList(GameUser + .CreateFromEntity); + return new GenericSerializableList(users, pageData); + } + + private async Task GetPlaylistCategory(PlaylistCategory playlistCategory, GameTokenEntity token, PaginationData pageData) + { + int totalPlaylists = await playlistCategory.GetItems(this.database, token).CountAsync(); + pageData.TotalElements = totalPlaylists; + IQueryable playlistQuery = playlistCategory.GetItems(this.database, token).ApplyPagination(pageData); + + List playlists = + (await playlistQuery.ToListAsync()).ToSerializableList(GamePlaylist + .CreateFromEntity); + return new GenericSerializableList(playlists, pageData); + } + + private async Task GetSlotCategory(SlotCategory slotCategory, GameTokenEntity token, SlotQueryBuilder queryBuilder, PaginationData pageData) + { + int totalSlots = await slotCategory.GetItems(this.database, token, queryBuilder).CountAsync(); + pageData.TotalElements = totalSlots; + IQueryable slotQuery = slotCategory.GetItems(this.database, token, queryBuilder).ApplyPagination(pageData); + + if (bool.TryParse(this.Request.Query["includePlayed"], out bool includePlayed) && !includePlayed) + { + slotQuery = slotQuery.Select(s => new SlotMetadata + { + Slot = s, + Played = this.database.VisitedLevels.Any(v => v.SlotId == s.SlotId && v.UserId == token.UserId), + }) + .Where(s => !s.Played) + .Select(s => s.Slot); + } + + if (this.Request.Query.ContainsKey("sort")) + { + string sort = (string?)this.Request.Query["sort"] ?? ""; + slotQuery = sort switch + { + "relevance" => slotQuery.ApplyOrdering(new SlotSortBuilder() + .AddSort(new UniquePlaysTotalSort()) + .AddSort(new LastUpdatedSort())), + "likes" => slotQuery.Select(s => new SlotMetadata + { + Slot = s, + ThumbsUp = this.database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1), + }) + .OrderByDescending(s => s.ThumbsUp) + .Select(s => s.Slot), + "hearts" => slotQuery.Select(s => new SlotMetadata + { + Slot = s, + Hearts = this.database.HeartedLevels.Count(h => h.SlotId == s.SlotId), + }) + .OrderByDescending(s => s.Hearts) + .Select(s => s.Slot), + "date" => slotQuery.ApplyOrdering(new SlotSortBuilder().AddSort(new FirstUploadedSort())), + "plays" => slotQuery.ApplyOrdering( + new SlotSortBuilder().AddSort(new UniquePlaysTotalSort()).AddSort(new TotalPlaysSort())), + _ => slotQuery, + }; + } + + List slots = + (await slotQuery.ToListAsync()).ToSerializableList(s => + SlotBase.CreateFromEntity(s, token)); + return new GenericSerializableList(slots, pageData); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs index b14ee83d..c3b190ec 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs @@ -2,13 +2,8 @@ using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; -using LBPUnion.ProjectLighthouse.Logging; -using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -148,95 +143,4 @@ public class CollectionController : ControllerBase return this.Ok(await this.GetUserPlaylists(targetUserId)); } - - [HttpGet("searches")] - [HttpGet("genres")] - public async Task GenresAndSearches() - { - GameTokenEntity token = this.GetToken(); - - UserEntity? user = await this.database.UserFromGameToken(token); - if (user == null) return this.Forbid(); - - List categories = new(); - - foreach (Category category in CategoryHelper.Categories.ToList()) - { - if(category is CategoryWithUser categoryWithUser) categories.Add(categoryWithUser.Serialize(this.database, user)); - else categories.Add(category.Serialize(this.database)); - } - - return this.Ok(new CategoryListResponse(categories, CategoryHelper.Categories.Count, 0, 1)); - } - - [HttpGet("searches/{endpointName}")] - public async Task GetCategorySlots(string endpointName, [FromQuery] int pageStart, [FromQuery] int pageSize, - [FromQuery] int players = 0, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? labelFilter3 = null, - [FromQuery] string? labelFilter4 = null, - [FromQuery] string? move = null, - [FromQuery] string? adventure = null - ) - { - GameTokenEntity token = this.GetToken(); - - UserEntity? user = await this.database.UserFromGameToken(token); - if (user == null) return this.Forbid(); - - Category? category = CategoryHelper.Categories.FirstOrDefault(c => c.Endpoint == endpointName); - if (category == null) return this.NotFound(); - - Logger.Debug("Found category " + category, LogArea.Category); - - List slots; - int totalSlots; - - if (category is CategoryWithUser categoryWithUser) - { - slots = (await categoryWithUser.GetSlots(this.database, user, pageStart, pageSize) - .ToListAsync()); - totalSlots = categoryWithUser.GetTotalSlots(this.database, user); - } - else - { - slots = category.GetSlots(this.database, pageStart, pageSize) - .ToList(); - totalSlots = category.GetTotalSlots(this.database); - } - - slots = this.filterSlots(slots, players + 1, labelFilter0, labelFilter1, labelFilter2, labelFilter3, labelFilter4, move, adventure); - - return this.Ok(new GenericSlotResponse("results", slots.ToSerializableList(s => SlotBase.CreateFromEntity(s, token)), totalSlots, pageStart + pageSize)); - } - - private List filterSlots(List slots, int players, string? labelFilter0 = null, string? labelFilter1 = null, string? labelFilter2 = null, string? labelFilter3 = null, string? labelFilter4 = null, string? move = null, string? adventure = null) - { - slots.RemoveAll(s => s.MinimumPlayers != players); - - if (labelFilter0 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter0)); - if (labelFilter1 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter1)); - if (labelFilter2 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter2)); - if (labelFilter3 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter3)); - if (labelFilter4 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter4)); - - if (move == "noneCan") - slots.RemoveAll(s => s.MoveRequired); - if (move == "allMust") - slots.RemoveAll(s => !s.MoveRequired); - - if (adventure == "noneCan") - slots.RemoveAll(s => s.IsAdventurePlanet); - if (adventure == "allMust") - slots.RemoveAll(s => !s.IsAdventurePlanet); - - return slots; - } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 2b902ee6..ca9dec79 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -1,11 +1,13 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Serialization; using LBPUnion.ProjectLighthouse.Types.Users; @@ -22,6 +24,7 @@ namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; public class ListController : ControllerBase { private readonly DatabaseContext database; + public ListController(DatabaseContext database) { this.database = database; @@ -32,31 +35,30 @@ public class ListController : ControllerBase #region Level Queue (lolcatftw) [HttpGet("slots/lolcatftw/{username}")] - public async Task GetQueuedLevels - ( - string username, - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] string? gameFilterType = null, - [FromQuery] int? players = null, - [FromQuery] bool? move = null, - [FromQuery] string? dateFilterType = null - ) + public async Task GetQueuedLevels(string username) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - List queuedLevels = (await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.Queue) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()) - .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + int targetUserId = await this.database.Users.Where(u => u.Username == username) + .Select(u => u.UserId) + .FirstOrDefaultAsync(); + if (targetUserId == 0) return this.BadRequest(); - int total = await this.database.QueuedLevels.CountAsync(q => q.UserId == token.UserId); - int start = pageStart + Math.Min(pageSize, 30); + pageData.TotalElements = await this.database.QueuedLevels.CountAsync(q => q.UserId == targetUserId); - return this.Ok(new GenericSlotResponse(queuedLevels, total, start)); + IQueryable baseQuery = this.database.QueuedLevels.Where(h => h.UserId == targetUserId) + .OrderByDescending(q => q.QueuedLevelId) + .Include(q => q.Slot) + .Select(q => q.Slot); + + List queuedLevels = await baseQuery.GetSlots(token, + this.FilterFromRequest(token), + pageData, + new SlotSortBuilder()); + + return this.Ok(new GenericSlotResponse(queuedLevels, pageData)); } [HttpPost("lolcatftw/add/user/{id:int}")] @@ -102,37 +104,33 @@ public class ListController : ControllerBase #region Hearted Levels [HttpGet("favouriteSlots/{username}")] - public async Task GetFavouriteSlots - ( - string username, - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] string? gameFilterType = null, - [FromQuery] int? players = null, - [FromQuery] bool? move = null, - [FromQuery] string? dateFilterType = null - ) + public async Task GetFavouriteSlots(string username) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - UserEntity? targetUser = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); - if (targetUser == null) return this.Forbid(); + int targetUserId = await this.database.Users.Where(u => u.Username == username) + .Select(u => u.UserId) + .FirstOrDefaultAsync(); + if (targetUserId == 0) return this.BadRequest(); - List heartedLevels = (await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.FavouriteSlots) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); - + pageData.TotalElements = await this.database.HeartedLevels.CountAsync(h => h.UserId == targetUserId); - int total = await this.database.HeartedLevels.CountAsync(q => q.UserId == targetUser.UserId); - int start = pageStart + Math.Min(pageSize, 30); + IQueryable baseQuery = this.database.HeartedLevels.Where(h => h.UserId == targetUserId) + .OrderByDescending(h => h.HeartedLevelId) + .Include(h => h.Slot) + .Select(h => h.Slot); - return this.Ok(new GenericSlotResponse("favouriteSlots", heartedLevels, total, start)); + List heartedLevels = await baseQuery.GetSlots(token, + this.FilterFromRequest(token), + pageData, + new SlotSortBuilder()); + + return this.Ok(new GenericSlotResponse("favouriteSlots", heartedLevels, pageData)); } - private const int FirstLbp2DeveloperSlotId = 124806; // This is the first known level slot GUID in LBP2. Feel free to change it if a lower one is found. + private const int firstLbp2DeveloperSlotId = 124806; // This is the first known level slot GUID in LBP2. Feel free to change it if a lower one is found. [HttpPost("favourite/slot/{slotType}/{id:int}")] public async Task AddFavouriteSlot(string slotType, int id) @@ -148,7 +146,7 @@ public class ListController : ControllerBase if (slotType == "developer") { - GameVersion slotGameVersion = (slot.InternalSlotId < FirstLbp2DeveloperSlotId) ? GameVersion.LittleBigPlanet1 : token.GameVersion; + GameVersion slotGameVersion = (slot.InternalSlotId < firstLbp2DeveloperSlotId) ? GameVersion.LittleBigPlanet1 : token.GameVersion; slot.GameVersion = slotGameVersion; } @@ -171,7 +169,7 @@ public class ListController : ControllerBase if (slotType == "developer") { - GameVersion slotGameVersion = (slot.InternalSlotId < FirstLbp2DeveloperSlotId) ? GameVersion.LittleBigPlanet1 : token.GameVersion; + GameVersion slotGameVersion = (slot.InternalSlotId < firstLbp2DeveloperSlotId) ? GameVersion.LittleBigPlanet1 : token.GameVersion; slot.GameVersion = slotGameVersion; } @@ -185,26 +183,27 @@ public class ListController : ControllerBase #region Hearted Playlists [HttpGet("favouritePlaylists/{username}")] - public async Task GetFavouritePlaylists(string username, [FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task GetFavouritePlaylists(string username) { - if (pageSize <= 0) return this.BadRequest(); int targetUserId = await this.database.UserIdFromUsername(username); if (targetUserId == 0) return this.Forbid(); + PaginationData pageData = this.Request.GetPaginationData(); + List heartedPlaylists = (await this.database.HeartedPlaylists.Where(p => p.UserId == targetUserId) .Include(p => p.Playlist) - .Include(p => p.Playlist.Creator) .OrderByDescending(p => p.HeartedPlaylistId) .Select(p => p.Playlist) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(GamePlaylist.CreateFromEntity); - int total = await this.database.HeartedPlaylists.CountAsync(p => p.UserId == targetUserId); + pageData.TotalElements = await this.database.HeartedPlaylists.CountAsync(p => p.UserId == targetUserId); return this.Ok(new GenericPlaylistResponse("favouritePlaylists", heartedPlaylists) { - Total = total, - HintStart = pageStart + Math.Min(pageSize, 30), + Total = pageData.TotalElements, + HintStart = pageData.HintStart, }); } @@ -241,26 +240,27 @@ public class ListController : ControllerBase #region Users [HttpGet("favouriteUsers/{username}")] - public async Task GetFavouriteUsers(string username, [FromQuery] int pageSize, [FromQuery] int pageStart) + public async Task GetFavouriteUsers(string username) { GameTokenEntity token = this.GetToken(); - UserEntity? targetUser = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); - if (targetUser == null) return this.Forbid(); + PaginationData pageData = this.Request.GetPaginationData(); - if (pageSize <= 0) return this.BadRequest(); + int targetUserId = await this.database.Users.Where(u => u.Username == username) + .Select(u => u.UserId) + .FirstOrDefaultAsync(); + if (targetUserId == 0) return this.BadRequest(); + + pageData.TotalElements = await this.database.HeartedProfiles.CountAsync(h => h.UserId == targetUserId); List heartedProfiles = (await this.database.HeartedProfiles.Include(h => h.HeartedUser) .OrderBy(h => h.HeartedProfileId) - .Where(h => h.UserId == targetUser.UserId) + .Where(h => h.UserId == targetUserId) .Select(h => h.HeartedUser) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(u => GameUser.CreateFromEntity(u, token.GameVersion)); - int total = await this.database.HeartedProfiles.CountAsync(h => h.UserId == targetUser.UserId); - - return this.Ok(new GenericUserResponse("favouriteUsers", heartedProfiles, total, pageStart + Math.Min(pageSize, 30))); + return this.Ok(new GenericUserResponse("favouriteUsers", heartedProfiles, pageData)); } [HttpPost("favourite/user/{username}")] @@ -288,85 +288,5 @@ public class ListController : ControllerBase return this.Ok(); } - #endregion - - #region Filtering - internal enum ListFilterType // used to collapse code that would otherwise be two separate functions - { - Queue, - FavouriteSlots, - } - - private static GameVersion getGameFilter(string? gameFilterType, GameVersion version) - { - return version switch - { - GameVersion.LittleBigPlanetVita => GameVersion.LittleBigPlanetVita, - GameVersion.LittleBigPlanetPSP => GameVersion.LittleBigPlanetPSP, - _ => gameFilterType switch - { - "lbp1" => GameVersion.LittleBigPlanet1, - "lbp2" => GameVersion.LittleBigPlanet2, - "lbp3" => GameVersion.LittleBigPlanet3, - "both" => GameVersion.LittleBigPlanet2, // LBP2 default option - null => GameVersion.LittleBigPlanet1, - _ => GameVersion.Unknown, - } - }; - } - - private IQueryable filterListByRequest(string? gameFilterType, string? dateFilterType, GameVersion version, string username, ListFilterType filterType) - { - if (version is GameVersion.LittleBigPlanetPSP or GameVersion.Unknown) - { - return this.database.Slots.ByGameVersion(version, false, true); - } - - long oldestTime = dateFilterType switch - { - "thisWeek" => DateTimeOffset.Now.AddDays(-7).ToUnixTimeMilliseconds(), - "thisMonth" => DateTimeOffset.Now.AddDays(-31).ToUnixTimeMilliseconds(), - _ => 0, - }; - - GameVersion gameVersion = getGameFilter(gameFilterType, version); - - // The filtering only cares if this isn't equal to 'both' - if (version == GameVersion.LittleBigPlanetVita) gameFilterType = "lbp2"; - - if (filterType == ListFilterType.Queue) - { - IQueryable whereQueuedLevels; - - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (gameFilterType == "both") - // Get game versions less than the current version - // Needs support for LBP3 ("both" = LBP1+2) - whereQueuedLevels = this.database.QueuedLevels.Where(q => q.User.Username == username) - .Where(q => q.Slot.Type == SlotType.User && !q.Slot.Hidden && q.Slot.GameVersion <= gameVersion && q.Slot.FirstUploaded >= oldestTime); - else - // Get game versions exactly equal to gamefiltertype - whereQueuedLevels = this.database.QueuedLevels.Where(q => q.User.Username == username) - .Where(q => q.Slot.Type == SlotType.User && !q.Slot.Hidden && q.Slot.GameVersion == gameVersion && q.Slot.FirstUploaded >= oldestTime); - - return whereQueuedLevels.OrderByDescending(q => q.QueuedLevelId).Include(q => q.Slot.Creator).Select(q => q.Slot).ByGameVersion(gameVersion, false, false, true); - } - - IQueryable whereHeartedLevels; - - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (gameFilterType == "both") - // Get game versions less than the current version - // Needs support for LBP3 ("both" = LBP1+2) - whereHeartedLevels = this.database.HeartedLevels.Where(h => h.User.Username == username) - .Where(h => (h.Slot.Type == SlotType.User || h.Slot.Type == SlotType.Developer) && !h.Slot.Hidden && h.Slot.GameVersion <= gameVersion && h.Slot.FirstUploaded >= oldestTime); - else - // Get game versions exactly equal to gamefiltertype - whereHeartedLevels = this.database.HeartedLevels.Where(h => h.User.Username == username) - .Where(h => (h.Slot.Type == SlotType.User || h.Slot.Type == SlotType.Developer) && !h.Slot.Hidden && h.Slot.GameVersion == gameVersion && h.Slot.FirstUploaded >= oldestTime); - - return whereHeartedLevels.OrderByDescending(h => h.HeartedLevelId).Include(h => h.Slot.Creator).Select(h => h.Slot).ByGameVersion(gameVersion, false, false, true); - } - #endregion Filtering } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index 1491cc0a..aa4e1c5b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -5,8 +5,8 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Serialization; -using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -28,7 +28,7 @@ public class ReviewController : ControllerBase // LBP1 rating [HttpPost("rate/user/{slotId:int}")] - public async Task Rate(int slotId, [FromQuery] int rating) + public async Task Rate(int slotId, int rating) { GameTokenEntity token = this.GetToken(); @@ -57,7 +57,7 @@ public class ReviewController : ControllerBase // LBP2 and beyond rating [HttpPost("dpadrate/user/{slotId:int}")] - public async Task DPadRate(int slotId, [FromQuery] int rating) + public async Task DPadRate(int slotId, int rating) { GameTokenEntity token = this.GetToken(); @@ -142,54 +142,47 @@ public class ReviewController : ControllerBase } [HttpGet("reviewsFor/user/{slotId:int}")] - public async Task ReviewsFor(int slotId, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10) + public async Task ReviewsFor(int slotId) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); - - GameVersion gameVersion = token.GameVersion; + PaginationData pageData = this.Request.GetPaginationData(); SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == slotId); if (slot == null) return this.BadRequest(); - List reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true) + List reviews = (await this.database.Reviews .Where(r => r.SlotId == slotId) .OrderByDescending(r => r.ThumbsUp - r.ThumbsDown) .ThenByDescending(r => r.Timestamp) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token)); - - return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageStart + Math.Min(pageSize, 30))); + return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageData.HintStart)); } [HttpGet("reviewsBy/{username}")] - public async Task ReviewsBy(string username, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10) + public async Task ReviewsBy(string username) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); - - GameVersion gameVersion = token.GameVersion; + PaginationData pageData = this.Request.GetPaginationData(); int targetUserId = await this.database.UserIdFromUsername(username); if (targetUserId == 0) return this.BadRequest(); - List reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true) + List reviews = (await this.database.Reviews .Where(r => r.ReviewerId == targetUserId) .OrderByDescending(r => r.Timestamp) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) + .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token)); - return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageStart)); + return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageData.HintStart)); } [HttpPost("rateReview/user/{slotId:int}/{username}")] - public async Task RateReview(int slotId, string username, [FromQuery] int rating = 0) + public async Task RateReview(int slotId, string username, int rating = 0) { GameTokenEntity token = this.GetToken(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs index b5e8102e..b4eb935c 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -1,14 +1,16 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; @@ -25,102 +27,29 @@ public class SearchController : ControllerBase } [HttpGet("searchLBP3")] - public Task SearchSlotsLBP3([FromQuery] int pageSize, [FromQuery] int pageStart, [FromQuery] string textFilter, - [FromQuery] int? players = 0, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? labelFilter3 = null, - [FromQuery] string? labelFilter4 = null, - [FromQuery] string? move = null, - [FromQuery] string? adventure = null) - => this.SearchSlots(textFilter, pageSize, pageStart, "results", false, players+1, labelFilter0, labelFilter1, labelFilter2, labelFilter3, labelFilter4, move, adventure); + public Task SearchSlotsLBP3([FromQuery] string textFilter) + => this.SearchSlots(textFilter, "results"); [HttpGet("search")] - public async Task SearchSlots( - [FromQuery] string query, - [FromQuery] int pageSize, - [FromQuery] int pageStart, - string? keyName = "slots", - bool crosscontrol = false, - [FromQuery] int? players = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? labelFilter3 = null, - [FromQuery] string? labelFilter4 = null, - [FromQuery] string? move = null, - [FromQuery] string? adventure = null - ) + public async Task SearchSlots([FromQuery] string query, string? keyName = "slots") { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); if (string.IsNullOrWhiteSpace(query)) return this.BadRequest(); - query = query.ToLower(); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - string[] keywords = query.Split(" "); + queryBuilder.AddFilter(new TextFilter(query)); - IQueryable dbQuery = this.database.Slots.ByGameVersion(token.GameVersion, false, true) - .Where(s => s.Type == SlotType.User && s.CrossControllerRequired == crosscontrol) - .OrderBy(s => !s.TeamPick) - .ThenByDescending(s => s.FirstUploaded) - .Where(s => s.SlotId >= 0); // dumb query to conv into IQueryable + pageData.TotalElements = await this.database.Slots.Where(queryBuilder.Build()).CountAsync(); - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (string keyword in keywords) - dbQuery = dbQuery.Where - ( - s => s.Name.ToLower().Contains(keyword) || - s.Description.ToLower().Contains(keyword) || - s.Creator!.Username.ToLower().Contains(keyword) || - s.SlotId.ToString().Equals(keyword) - ); + List slots = await this.database.Slots.Include(s => s.Creator) + .GetSlots(token, queryBuilder, pageData, new SlotSortBuilder()); - List slots = (await dbQuery.Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()); - - slots = filterSlots(slots, players, labelFilter0, labelFilter1, labelFilter2, labelFilter3, labelFilter4, move, adventure); - - return this.Ok(new GenericSlotResponse(keyName, slots.ToSerializableList(s => SlotBase.CreateFromEntity(s, token)), await dbQuery.CountAsync(), 0)); + return this.Ok(new GenericSlotResponse(keyName, slots, pageData)); } // /LITTLEBIGPLANETPS3_XML?pageStart=1&pageSize=10&resultTypes[]=slot&resultTypes[]=playlist&resultTypes[]=user&adventure=dontCare&textFilter=qwer - - private List filterSlots(List slots, int? players = null, string? labelFilter0 = null, string? labelFilter1 = null, string? labelFilter2 = null, string? labelFilter3 = null, string? labelFilter4 = null, string? move = null, string? adventure = null) - { - if (players != null) - slots.RemoveAll(s => s.MinimumPlayers != players); - - if (labelFilter0 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter0)); - if (labelFilter1 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter1)); - if (labelFilter2 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter2)); - if (labelFilter3 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter3)); - if (labelFilter4 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter4)); - - if (move == "false") - slots.RemoveAll(s => s.MoveRequired); - if (move == "only") - slots.RemoveAll(s => !s.MoveRequired); - - if (move == "noneCan") - slots.RemoveAll(s => s.MoveRequired); - if (move == "allMust") - slots.RemoveAll(s => !s.MoveRequired); - - if (adventure == "noneCan") - slots.RemoveAll(s => s.IsAdventurePlanet); - if (adventure == "allMust") - slots.RemoveAll(s => !s.IsAdventurePlanet); - - return slots; - } } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index 998636e0..e9ec67a5 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -1,10 +1,16 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Configuration; +using System.Linq.Expressions; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; using LBPUnion.ProjectLighthouse.Types.Misc; @@ -23,34 +29,31 @@ namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; public class SlotsController : ControllerBase { private readonly DatabaseContext database; + public SlotsController(DatabaseContext database) { this.database = database; } [HttpGet("slots/by")] - public async Task SlotsBy([FromQuery(Name = "u")] string username, [FromQuery] int pageStart, [FromQuery] int pageSize, [FromQuery] bool crosscontrol = false) + public async Task SlotsBy([FromQuery(Name = "u")] string username) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); - int targetUserId = await this.database.UserIdFromUsername(username); if (targetUserId == 0) return this.NotFound(); - int usedSlots = this.database.Slots.Count(s => s.CreatorId == targetUserId); + PaginationData pageData = this.Request.GetPaginationData(); - List slots = (await this.database.Slots.Where(s => s.CreatorId == targetUserId) - .ByGameVersion(token.GameVersion, token.UserId == targetUserId) - .Where(match => match.CrossControllerRequired == crosscontrol) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, usedSlots)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); - - int start = pageStart + Math.Min(pageSize, usedSlots); - int total = await this.database.Slots.CountAsync(s => s.CreatorId == targetUserId && s.CrossControllerRequired == crosscontrol); + pageData.TotalElements = await this.database.Slots.CountAsync(s => s.CreatorId == targetUserId); - return this.Ok(new GenericSlotResponse("slots", slots, total, start)); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token).AddFilter(new CreatorFilter(targetUserId)); + + SlotSortBuilder sortBuilder = new SlotSortBuilder().AddSort(new FirstUploadedSort()); + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse("slots", slots, pageData)); } [HttpGet("slotList")] @@ -61,7 +64,7 @@ public class SlotsController : ControllerBase List slots = new(); foreach (int slotId in slotIds) { - SlotEntity? slot = await this.database.Slots.Include(t => t.Creator).Where(t => t.SlotId == slotId && t.Type == SlotType.User).FirstOrDefaultAsync(); + SlotEntity? slot = await this.database.Slots.Where(t => t.SlotId == slotId && t.Type == SlotType.User).FirstOrDefaultAsync(); if (slot == null) { slot = await this.database.Slots.Where(t => t.InternalSlotId == slotId && t.Type == SlotType.Developer).FirstOrDefaultAsync(); @@ -102,7 +105,7 @@ public class SlotsController : ControllerBase } [HttpGet("s/developer/{id:int}")] - public async Task SDev(int id) + public async Task DeveloperSlot(int id) { GameTokenEntity token = this.GetToken(); @@ -113,13 +116,12 @@ public class SlotsController : ControllerBase } [HttpGet("s/user/{id:int}")] - public async Task SUser(int id) + public async Task UserSlot(int id) { GameTokenEntity token = this.GetToken(); - GameVersion gameVersion = token.GameVersion; - - SlotEntity? slot = await this.database.Slots.ByGameVersion(gameVersion, true, true).FirstOrDefaultAsync(s => s.SlotId == id); + SlotEntity? slot = await this.database.Slots.Where(this.GetDefaultFilters(token).Build()) + .FirstOrDefaultAsync(s => s.SlotId == id); if (slot == null) return this.NotFound(); @@ -127,66 +129,40 @@ public class SlotsController : ControllerBase } [HttpGet("slots/cool")] - public async Task Lbp1CoolSlots([FromQuery] int page) - { - const int pageSize = 30; - return await this.CoolSlots((page - 1) * pageSize, pageSize); - } + public async Task Lbp1CoolSlots() => await this.CoolSlots(); [HttpGet("slots/lbp2cool")] - public async Task CoolSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int players = 1, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] int? page = null, - [FromQuery] bool crosscontrol = false - ) - { - if (page != null) pageStart = (int)page * 30; - // bit of a better placeholder until we can track average user interaction with /stream endpoint - return await this.ThumbsSlots(pageStart, Math.Min(pageSize, 30), players, gameFilterType, "thisMonth", - labelFilter0, labelFilter1, labelFilter2, move, crosscontrol); - } + public async Task CoolSlots() => await this.ThumbsSlots(); [HttpGet("slots")] - public async Task NewestSlots([FromQuery] int pageStart, [FromQuery] int pageSize, [FromQuery] bool crosscontrol = false) + public async Task NewestSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - GameVersion gameVersion = token.GameVersion; + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) - .Where(s => s.CrossControllerRequired == crosscontrol) - .OrderByDescending(s => s.FirstUploaded) - .ThenByDescending(s => s.SlotId) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); - return this.Ok(new GenericSlotResponse(slots, total, start)); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new FirstUploadedSort()); + sortBuilder.AddSort(new SlotIdSort()); + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/like/{slotType}/{slotId:int}")] - public async Task SimilarSlots([FromRoute] string slotType, [FromRoute] int slotId, [FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task SimilarSlots([FromRoute] string slotType, [FromRoute] int slotId) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); if (slotType != "user") return this.BadRequest(); - GameVersion gameVersion = token.GameVersion; - SlotEntity? targetSlot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == slotId); if (targetSlot == null) return this.BadRequest(); @@ -198,275 +174,180 @@ public class SlotsController : ControllerBase .Select(r => r.SlotId) .ToList(); - List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) - .Where(s => slotIdsWithTag.Contains(s.SlotId)) - .OrderByDescending(s => s.PlaysLBP1) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + pageData.TotalElements = slotIdsWithTag.Count; - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = slotIdsWithTag.Count; + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token).AddFilter(0, new SlotIdFilter(slotIdsWithTag)); - return this.Ok(new GenericSlotResponse(slots, total, start)); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new PlaysForGameSort(GameVersion.LittleBigPlanet1)); + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/highestRated")] - public async Task HighestRatedSlots([FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task HighestRatedSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - GameVersion gameVersion = token.GameVersion; + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) - .Select(s => new SlotMetadata - { - Slot = s, - RatingLbp1 = this.database.RatedLevels.Where(r => r.SlotId == s.SlotId).Average(r => (double?)r.RatingLBP1) ?? 3.0, - }) - .OrderByDescending(s => s.RatingLbp1) - .Select(s => s.Slot) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCount(this.database); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new RatingLBP1Sort()); - return this.Ok(new GenericSlotResponse(slots, total, start)); + Expression> selectorFunc = s => new SlotMetadata + { + Slot = s, + RatingLbp1 = this.database.RatedLevels.Where(r => r.SlotId == s.SlotId) + .Average(r => (double?)r.RatingLBP1) ?? 3.0, + }; + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder, selectorFunc); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/tag")] - public async Task SimilarSlots([FromQuery] string tag, [FromQuery] int pageStart, [FromQuery] int pageSize) + public async Task SimilarSlots([FromQuery] string tag) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); List slotIdsWithTag = await this.database.RatedLevels.Where(r => r.TagLBP1.Length > 0) .Where(r => r.TagLBP1 == tag) .Select(s => s.SlotId) .ToListAsync(); - List slots = (await this.database.Slots.Where(s => slotIdsWithTag.Contains(s.SlotId)) - .ByGameVersion(token.GameVersion, false, true) - .OrderByDescending(s => s.PlaysLBP1) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + pageData.TotalElements = slotIdsWithTag.Count; - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = slotIdsWithTag.Count; + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new PlaysForGameSort(GameVersion.LittleBigPlanet1)); - return this.Ok(new GenericSlotResponse(slots, total, start)); + List slots = await this.database.GetSlots(token, this.FilterFromRequest(token), pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/mmpicks")] - public async Task TeamPickedSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int players, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? dateFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] bool crosscontrol = false - ) + public async Task TeamPickedSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - List slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) - .Where(s => s.TeamPick && s.CrossControllerRequired == crosscontrol) - .OrderByDescending(s => s.LastUpdated) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()), players, labelFilter0, labelFilter1, labelFilter2, move).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.TeamPickCountForGame(this.database, token.GameVersion, crosscontrol); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token).AddFilter(new TeamPickFilter()); - return this.Ok(new GenericSlotResponse(slots, total, start)); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); + + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new LastUpdatedSort()); + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/lbp2luckydip")] - public async Task LuckyDipSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int seed, - [FromQuery] int players = 1, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? dateFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] bool crosscontrol = false - ) + public async Task LuckyDipSlots([FromQuery] int seed) { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - GameVersion gameVersion = token.GameVersion; + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - const double biasFactor = .8f; - List slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) - .Where(s => s.CrossControllerRequired == crosscontrol) - .OrderByDescending(s => EF.Functions.Random() * (s.FirstUploaded * biasFactor)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()), players, labelFilter0, labelFilter1, labelFilter2, move).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new RandomFirstUploadedSort()); - return this.Ok(new GenericSlotResponse(slots, total, start)); + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/thumbs")] - public async Task ThumbsSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int players, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? dateFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] bool crosscontrol = false - ) + public async Task ThumbsSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - List slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) - .Where(s => s.CrossControllerRequired == crosscontrol) - .Select(s => new SlotMetadata - { - Slot = s, - ThumbsUp = this.database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1), - }) - .OrderByDescending(s => s.ThumbsUp) - .ThenBy(_ => EF.Functions.Random()) - .Select(s => s.Slot) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()), players, labelFilter0, labelFilter1, labelFilter2, move).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - return this.Ok(new GenericSlotResponse(slots, total, start)); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new ThumbsUpSort()); + + Expression> selectorFunc = s => new SlotMetadata + { + Slot = s, + ThumbsUp = this.database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1), + }; + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder, selectorFunc); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/mostUniquePlays")] - public async Task MostUniquePlaysSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int players, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] string? dateFilterType = null, - [FromQuery] bool crosscontrol = false - ) + public async Task MostUniquePlaysSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - string game = getGameFilter(gameFilterType, token.GameVersion) switch - { - GameVersion.LittleBigPlanet1 => "LBP1", - GameVersion.LittleBigPlanet2 => "LBP2", - GameVersion.LittleBigPlanet3 => "LBP3", - GameVersion.LittleBigPlanetVita => "LBP2", - _ => "", - }; + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - string colName = $"Plays{game}Unique"; + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - List slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) - .Where(s => s.CrossControllerRequired == crosscontrol) - .OrderByDescending(s => EF.Property(s, colName)) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()), players, labelFilter0, labelFilter1, labelFilter2, move).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new UniquePlaysForGameSort(token.GameVersion)); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder); - return this.Ok(new GenericSlotResponse(slots, total, start)); + return this.Ok(new GenericSlotResponse(slots, pageData)); } [HttpGet("slots/mostHearted")] - public async Task MostHeartedSlots - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] int players, - [FromQuery] string? gameFilterType = null, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] string? dateFilterType = null, - [FromQuery] bool crosscontrol = false - ) + public async Task MostHeartedSlots() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); - List slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) - .Where(s => s.CrossControllerRequired == crosscontrol) - .Select(s => new SlotMetadata - { - Slot = s, - Hearts = this.database.HeartedLevels.Count(r => r.SlotId == s.SlotId), - }) - .OrderByDescending(s => s.Hearts) - .Select(s => s.Slot) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .ToListAsync()), players, labelFilter0, labelFilter1, labelFilter2, move).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); + pageData.TotalElements = await StatisticsHelper.SlotCount(this.database, queryBuilder); - return this.Ok(new GenericSlotResponse(slots, total, start)); + SlotSortBuilder sortBuilder = new(); + sortBuilder.AddSort(new HeartsSort()); + + Expression> selectorFunc = s => new SlotMetadata + { + Slot = s, + Hearts = this.database.HeartedLevels.Count(r => r.SlotId == s.SlotId), + }; + + List slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder, selectorFunc); + + return this.Ok(new GenericSlotResponse(slots, pageData)); } // /slots/busiest?pageStart=1&pageSize=30&gameFilterType=both&players=1&move=true [HttpGet("slots/busiest")] - public async Task BusiestLevels - ( - [FromQuery] int pageStart, - [FromQuery] int pageSize, - [FromQuery] string? gameFilterType = null, - [FromQuery] int players = 1, - [FromQuery] string? labelFilter0 = null, - [FromQuery] string? labelFilter1 = null, - [FromQuery] string? labelFilter2 = null, - [FromQuery] string? move = null, - [FromQuery] bool crosscontrol = false - ) + public async Task BusiestLevels() { GameTokenEntity token = this.GetToken(); - if (pageSize <= 0) return this.BadRequest(); + PaginationData pageData = this.Request.GetPaginationData(); Dictionary playersBySlotId = new(); @@ -484,98 +365,15 @@ public class SlotsController : ControllerBase playersBySlotId.Add(room.Slot.SlotId, playerCount); } - IEnumerable orderedPlayersBySlotId = playersBySlotId - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 30)) - .OrderByDescending(kvp => kvp.Value) - .Select(kvp => kvp.Key); - - List slots = new(); + pageData.TotalElements = playersBySlotId.Count; - foreach (int slotId in orderedPlayersBySlotId) - { - SlotEntity? slot = await this.database.Slots.ByGameVersion(token.GameVersion, false, true) - .Where(s => s.SlotId == slotId && s.CrossControllerRequired == crosscontrol) - .FirstOrDefaultAsync(); - if (slot == null) continue; // shouldn't happen ever unless the room is borked - - slots.Add(slot); - } + List orderedPlayersBySlotId = playersBySlotId.OrderByDescending(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); - slots = this.filterSlots(slots, players, labelFilter0, labelFilter1, labelFilter2, move); + SlotQueryBuilder queryBuilder = this.FilterFromRequest(token); + queryBuilder.AddFilter(0, new SlotIdFilter(orderedPlayersBySlotId)); - int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); - int total = playersBySlotId.Count; + List slots = await this.database.GetSlots(token, queryBuilder, pageData, new SlotSortBuilder()); - return this.Ok(new GenericSlotResponse(slots.ToSerializableList(s => SlotBase.CreateFromEntity(s, token)), total, start)); - } - - private List filterSlots(List slots, int players, string? labelFilter0 = null, string? labelFilter1 = null, string? labelFilter2 = null, string? move = null) - { - slots.RemoveAll(s => s.MinimumPlayers != players); - - if (labelFilter0 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter0)); - if (labelFilter1 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter1)); - if (labelFilter2 != null) - slots.RemoveAll(s => !s.AuthorLabels.Split(',').ToList().Contains(labelFilter2)); - - if (move == "false") - slots.RemoveAll(s => s.MoveRequired); - if (move == "only") - slots.RemoveAll(s => !s.MoveRequired); - - return slots; - } - - private static GameVersion getGameFilter(string? gameFilterType, GameVersion version) - { - return version switch - { - GameVersion.LittleBigPlanetVita => GameVersion.LittleBigPlanetVita, - GameVersion.LittleBigPlanetPSP => GameVersion.LittleBigPlanetPSP, - _ => gameFilterType switch - { - "lbp1" => GameVersion.LittleBigPlanet1, - "lbp2" => GameVersion.LittleBigPlanet2, - "lbp3" => GameVersion.LittleBigPlanet3, - "both" => GameVersion.LittleBigPlanet2, // LBP2 default option - null => GameVersion.LittleBigPlanet1, - _ => GameVersion.Unknown, - }, - }; - } - - private IQueryable filterByRequest(string? gameFilterType, string? dateFilterType, GameVersion version) - { - if (version == GameVersion.LittleBigPlanetVita || version == GameVersion.LittleBigPlanetPSP || version == GameVersion.Unknown) - { - return this.database.Slots.ByGameVersion(version, false, true); - } - - string _dateFilterType = dateFilterType ?? ""; - - long oldestTime = _dateFilterType switch - { - "thisWeek" => DateTimeOffset.Now.AddDays(-7).ToUnixTimeMilliseconds(), - "thisMonth" => DateTimeOffset.Now.AddDays(-31).ToUnixTimeMilliseconds(), - _ => 0, - }; - - GameVersion gameVersion = getGameFilter(gameFilterType, version); - - IQueryable whereSlots; - - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (gameFilterType == "both") - // Get game versions less than the current version - // Needs support for LBP3 ("both" = LBP1+2) - whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && !s.Hidden && s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime); - else - // Get game versions exactly equal to gamefiltertype - whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && !s.Hidden && s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime); - - return whereSlots.Include(s => s.Creator); + return this.Ok(new GenericSlotResponse(slots, pageData)); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs index 2e8932ba..3eb828e4 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/StatisticsController.cs @@ -3,8 +3,10 @@ using LBPUnion.ProjectLighthouse.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; using LBPUnion.ProjectLighthouse.Types.Serialization; -using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; @@ -14,7 +16,6 @@ namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; [Produces("text/plain")] public class StatisticsController : ControllerBase { - private readonly DatabaseContext database; public StatisticsController(DatabaseContext database) @@ -23,7 +24,7 @@ public class StatisticsController : ControllerBase } [HttpGet("playersInPodCount")] -public IActionResult PlayersInPodCount() => this.Ok(StatisticsHelper.RoomCountForPlatform(this.GetToken().Platform).ToString()); + public IActionResult PlayersInPodCount() => this.Ok(StatisticsHelper.RoomCountForPlatform(this.GetToken().Platform).ToString()); [HttpGet("totalPlayerCount")] public async Task TotalPlayerCount() => this.Ok((await StatisticsHelper.RecentMatchesForGame(this.database, this.GetToken().GameVersion)).ToString()); @@ -32,12 +33,19 @@ public IActionResult PlayersInPodCount() => this.Ok(StatisticsHelper.RoomCountFo [Produces("text/xml")] public async Task PlanetStats() { - int totalSlotCount = await StatisticsHelper.SlotCountForGame(this.database, this.GetToken().GameVersion); - int mmPicksCount = await StatisticsHelper.TeamPickCountForGame(this.database, this.GetToken().GameVersion); + SlotQueryBuilder defaultFilter = this.GetDefaultFilters(this.GetToken()); + int totalSlotCount = await StatisticsHelper.SlotCount(this.database, defaultFilter); + defaultFilter.AddFilter(new TeamPickFilter()); + int mmPicksCount = await StatisticsHelper.SlotCount(this.database, defaultFilter); return this.Ok(new PlanetStatsResponse(totalSlotCount, mmPicksCount)); } [HttpGet("planetStats/totalLevelCount")] - public async Task TotalLevelCount() => this.Ok((await StatisticsHelper.SlotCountForGame(this.database, this.GetToken().GameVersion)).ToString()); + public async Task TotalLevelCount() + { + SlotQueryBuilder defaultFilter = this.GetDefaultFilters(this.GetToken()); + int totalSlotCount = await StatisticsHelper.SlotCount(this.database, defaultFilter); + return this.Ok(totalSlotCount.ToString()); + } } diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs new file mode 100644 index 00000000..92f6f092 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Extensions/ControllerExtensions.cs @@ -0,0 +1,183 @@ +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; + +public static class ControllerExtensions +{ + private static GameVersion GetGameFilter(string? gameFilterType, GameVersion version) + { + return version switch + { + GameVersion.LittleBigPlanetVita => GameVersion.LittleBigPlanetVita, + GameVersion.LittleBigPlanetPSP => GameVersion.LittleBigPlanetPSP, + _ => gameFilterType switch + { + "lbp1" => GameVersion.LittleBigPlanet1, + "lbp2" => GameVersion.LittleBigPlanet2, + "lbp3" => GameVersion.LittleBigPlanet3, + "both" => GameVersion.LittleBigPlanet2, // LBP2 default option + null => GameVersion.LittleBigPlanet1, + _ => GameVersion.Unknown, + }, + }; + } + + public static SlotQueryBuilder GetDefaultFilters(this ControllerBase controller, GameTokenEntity token) => + new SlotQueryBuilder().AddFilter(new GameVersionFilter(token.GameVersion)) + .AddFilter(new SubLevelFilter(token.UserId)) + .AddFilter(new HiddenSlotFilter()) + .AddFilter(new SlotTypeFilter(SlotType.User)); + + public static SlotQueryBuilder FilterFromRequest(this ControllerBase controller, GameTokenEntity token) + { + SlotQueryBuilder queryBuilder = new(); + + List authorLabels = new(); + for (int i = 0; i < 3; i++) + { + string? label = controller.Request.Query[$"labelFilter{i}"]; + if (label == null) continue; + authorLabels.Add(label); + } + + if (authorLabels.Count > 0) queryBuilder.AddFilter(new AuthorLabelFilter(authorLabels.ToArray())); + + if (int.TryParse(controller.Request.Query["players"], out int minPlayers) && minPlayers >= 1) + { + // LBP3 starts counting at 0 + if (token.GameVersion == GameVersion.LittleBigPlanet3) minPlayers++; + + queryBuilder.AddFilter(new PlayerCountFilter(minPlayers)); + } + + if (controller.Request.Query.ContainsKey("textFilter")) + { + string textFilter = (string?)controller.Request.Query["textFilter"] ?? ""; + + if (!string.IsNullOrWhiteSpace(textFilter)) queryBuilder.AddFilter(new TextFilter(textFilter)); + } + + if (controller.Request.Query.ContainsKey("dateFilterType")) + { + string dateFilter = (string?)controller.Request.Query["dateFilterType"] ?? ""; + long oldestTime = dateFilter switch + { + "thisWeek" => DateTimeOffset.UtcNow.AddDays(-7).ToUnixTimeMilliseconds(), + "thisMonth" => DateTimeOffset.UtcNow.AddDays(-31).ToUnixTimeMilliseconds(), + _ => 0, + }; + if (oldestTime != 0) queryBuilder.AddFilter(new FirstUploadedFilter(oldestTime)); + } + + if (token.GameVersion != GameVersion.LittleBigPlanet3) + { + if (controller.Request.Query.ContainsKey("move")) + { + string moveFilter = (string?)controller.Request.Query["move"] ?? ""; + // By default this will include levels with move so we don't handle true + switch (moveFilter) + { + case "false": + queryBuilder.AddFilter(new ExcludeMovePackFilter()); + break; + case "only": + queryBuilder.AddFilter(new MovePackFilter()); + break; + } + } + + if (bool.TryParse(controller.Request.Query["move"], out bool movePack) && !movePack) + queryBuilder.AddFilter(new ExcludeMovePackFilter()); + + if (bool.TryParse(controller.Request.Query["crosscontrol"], out bool crossControl) && crossControl) + queryBuilder.AddFilter(new CrossControlFilter()); + + GameVersion targetVersion = token.GameVersion; + + if (controller.Request.Query.ContainsKey("gameFilterType")) + { + string gameFilter = (string?)controller.Request.Query["gameFilterType"] ?? ""; + GameVersion filterVersion = GetGameFilter(gameFilter, targetVersion); + // Don't serve lbp3 levels to lbp2 just cause of the game filter + if (filterVersion <= targetVersion) + { + targetVersion = filterVersion; + } + } + queryBuilder.AddFilter(new GameVersionFilter(targetVersion)); + } + else if (token.GameVersion == GameVersion.LittleBigPlanet3) + { + void ParseLbp3Query(string key, Action allMust, Action noneCan, Action dontCare) + { + if (!controller.Request.Query.ContainsKey(key)) return; + + string value = (string?)controller.Request.Query[key] ?? "dontCare"; + switch (value) + { + case "allMust": + allMust(); + break; + case "noneCan": + noneCan(); + break; + case "dontCare": + dontCare(); + break; + } + } + + ParseLbp3Query("adventure", + () => queryBuilder.AddFilter(new AdventureFilter()), + () => queryBuilder.AddFilter(new ExcludeAdventureFilter()), + () => + { }); + + ParseLbp3Query("move", + () => queryBuilder.AddFilter(new MovePackFilter()), + () => queryBuilder.AddFilter(new ExcludeMovePackFilter()), + () => + { }); + + string[]? ParseLbp3ArrayQuery(string key) + { + return !controller.Request.Query.TryGetValue($"{key}[]", out StringValues keys) + ? null + : keys.Where(s => s != null).Select(s => s!).ToArray(); + } + + string[]? gameFilters = ParseLbp3ArrayQuery("gameFilter"); + if (gameFilters != null) + { + queryBuilder.AddFilter(new GameVersionListFilter(gameFilters + .Select(s => GetGameFilter(s, token.GameVersion)) + .ToArray())); + } + else + { + queryBuilder.AddFilter(new GameVersionFilter(GameVersion.LittleBigPlanet3)); + } + + string[]? resultFilters = ParseLbp3ArrayQuery("resultType"); + if (resultFilters != null) + { + queryBuilder.AddFilter(new ResultTypeFilter(resultFilters)); + } + } + + if (token.GameVersion != GameVersion.LittleBigPlanet1) + queryBuilder.AddFilter(new ExcludeLBP1OnlyFilter(token.UserId, token.GameVersion)); + + queryBuilder.AddFilter(new SubLevelFilter(token.UserId)); + queryBuilder.AddFilter(new HiddenSlotFilter()); + queryBuilder.AddFilter(new SlotTypeFilter(SlotType.User)); + + return queryBuilder; + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs new file mode 100644 index 00000000..bef6943e --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Extensions/DatabaseContextExtensions.cs @@ -0,0 +1,59 @@ +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Misc; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; + +public static class DatabaseContextExtensions +{ + public static async Task> GetSlots + ( + this IQueryable queryable, + GameTokenEntity token, + SlotQueryBuilder queryBuilder, + PaginationData pageData, + ISortBuilder sortBuilder + ) => + (await queryable.Where(queryBuilder.Build()) + .ApplyOrdering(sortBuilder) + .ApplyPagination(pageData) + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + + public static async Task> GetSlots + ( + this DatabaseContext database, + GameTokenEntity token, + SlotQueryBuilder queryBuilder, + PaginationData pageData, + ISortBuilder sortBuilder + ) => + (await database.Slots.Where(queryBuilder.Build()) + .ApplyOrdering(sortBuilder) + .ApplyPagination(pageData) + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + + public static async Task> GetSlots + ( + this DatabaseContext database, + GameTokenEntity token, + SlotQueryBuilder queryBuilder, + PaginationData pageData, + ISortBuilder sortBuilder, + Expression> selectorFunction + ) => + (await database.Slots.Where(queryBuilder.Build()) + .AsQueryable() + .Select(selectorFunction) + .ApplyOrdering(sortBuilder) + .Select(s => s.Slot) + .ApplyPagination(pageData) + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs index 74c40005..404e458e 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs @@ -15,6 +15,8 @@ public static class CategoryHelper Categories.Add(new NewestLevelsCategory()); Categories.Add(new MostPlayedCategory()); Categories.Add(new HighestRatedCategory()); + Categories.Add(new MyHeartedCreatorsCategory()); + Categories.Add(new MyPlaylistsCategory()); Categories.Add(new QueueCategory()); Categories.Add(new HeartedCategory()); Categories.Add(new LuckyDipCategory()); diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryWithUser.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryWithUser.cs deleted file mode 100644 index c051f26d..00000000 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryWithUser.cs +++ /dev/null @@ -1,62 +0,0 @@ -#nullable enable -using System.Diagnostics; -using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Logging; -using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Logging; -using LBPUnion.ProjectLighthouse.Types.Serialization; -using LBPUnion.ProjectLighthouse.Types.Users; - -namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; - -public abstract class CategoryWithUser : Category -{ - public abstract SlotEntity? GetPreviewSlot(DatabaseContext database, UserEntity user); - public override SlotEntity? GetPreviewSlot(DatabaseContext database) - { - #if DEBUG - Logger.Error("tried to get preview slot without user on CategoryWithUser", LogArea.Category); - if (Debugger.IsAttached) Debugger.Break(); - #endif - return null; - } - - public abstract int GetTotalSlots(DatabaseContext database, UserEntity user); - public override int GetTotalSlots(DatabaseContext database) - { - #if DEBUG - Logger.Error("tried to get total slots without user on CategoryWithUser", LogArea.Category); - if (Debugger.IsAttached) Debugger.Break(); - #endif - return -1; - } - - public abstract IQueryable GetSlots(DatabaseContext database, UserEntity user, int pageStart, int pageSize); - public override IList GetSlots(DatabaseContext database, int pageStart, int pageSize) - { - #if DEBUG - Logger.Error("tried to get slots without user on CategoryWithUser", LogArea.Category); - if (Debugger.IsAttached) Debugger.Break(); - #endif - return new List(); - } - - public new string Serialize(DatabaseContext database) - { - Logger.Error("tried to serialize without user on CategoryWithUser", LogArea.Category); - return string.Empty; - } - - public GameCategory Serialize(DatabaseContext database, UserEntity user) - { - List slots = new(); - SlotEntity? previewSlot = this.GetPreviewSlot(database, user); - if (previewSlot != null) - slots.Add(SlotBase.CreateFromEntity(previewSlot, GameVersion.LittleBigPlanet3, user.UserId)); - - int totalSlots = this.GetTotalSlots(database, user); - return GameCategory.CreateFromEntity(this, new GenericSlotResponse(slots, totalSlots, 2)); - } -} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs index c281739a..b483f546 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/CustomCategory.cs @@ -1,16 +1,15 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class CustomCategory : Category +public class CustomCategory : SlotCategory { - - public List SlotIds; + private readonly List slotIds; public CustomCategory(string name, string description, string endpoint, string icon, IEnumerable slotIds) { this.Name = name; @@ -18,7 +17,7 @@ public class CustomCategory : Category this.IconHash = icon; this.Endpoint = endpoint; - this.SlotIds = slotIds.ToList(); + this.slotIds = slotIds.ToList(); } public CustomCategory(DatabaseCategoryEntity category) @@ -28,16 +27,19 @@ public class CustomCategory : Category this.IconHash = category.IconHash; this.Endpoint = category.Endpoint; - this.SlotIds = category.SlotIds.ToList(); + this.slotIds = category.SlotIds.ToList(); } public sealed override string Name { get; set; } public sealed override string Description { get; set; } public sealed override string IconHash { get; set; } public sealed override string Endpoint { get; set; } - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => database.Slots.FirstOrDefault(s => s.SlotId == this.SlotIds[0] && !s.CrossControllerRequired); - public override IQueryable GetSlots - (DatabaseContext database, int pageStart, int pageSize) - => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3).Where(s => this.SlotIds.Contains(s.SlotId) && !s.CrossControllerRequired); - public override int GetTotalSlots(DatabaseContext database) => this.SlotIds.Count; + + public override string Tag => "custom_category"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity entity, SlotQueryBuilder queryBuilder) + { + queryBuilder.Clone().AddFilter(new SlotIdFilter(this.slotIds)); + return database.Slots.Where(queryBuilder.Build()); + } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/HeartedCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/HeartedCategory.cs index 97ba3a6d..0a9eeee1 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/HeartedCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/HeartedCategory.cs @@ -1,38 +1,22 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class HeartedCategory : CategoryWithUser +public class HeartedCategory : SlotCategory { public override string Name { get; set; } = "My Hearted Content"; public override string Description { get; set; } = "Content you've hearted"; public override string IconHash { get; set; } = "g820611"; - public override string Endpoint { get; set; } = "hearted"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database, UserEntity user) // note: developer slots act up in LBP3 when listed here, so I omitted it - => database.HeartedLevels.Where(h => h.UserId == user.UserId) - .Where(h => h.Slot.Type == SlotType.User && !h.Slot.Hidden && h.Slot.GameVersion <= GameVersion.LittleBigPlanet3 && !h.Slot.CrossControllerRequired) - .OrderByDescending(h => h.HeartedLevelId) - .Include(h => h.Slot.Creator) - .Select(h => h.Slot) - .ByGameVersion(GameVersion.LittleBigPlanet3, false, false, true) - .FirstOrDefault(); + public override string Endpoint { get; set; } = "hearted_levels"; + public override string Tag => "my_hearted_levels"; - public override IQueryable GetSlots(DatabaseContext database, UserEntity user, int pageStart, int pageSize) - => database.HeartedLevels.Where(h => h.UserId == user.UserId) - .Where(h => h.Slot.Type == SlotType.User && !h.Slot.Hidden && h.Slot.GameVersion <= GameVersion.LittleBigPlanet3 && !h.Slot.CrossControllerRequired) + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.HeartedLevels.Where(h => h.UserId == token.UserId) .OrderByDescending(h => h.HeartedLevelId) - .Include(h => h.Slot.Creator) .Select(h => h.Slot) - .ByGameVersion(GameVersion.LittleBigPlanet3, false, false, true) - .Skip(Math.Max(0, pageStart)) - .Take(Math.Min(pageSize, 20)); - - public override int GetTotalSlots(DatabaseContext database, UserEntity user) => database.HeartedLevels.Count(h => h.UserId == user.UserId); + .Where(queryBuilder.Build()); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/HighestRatedCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/HighestRatedCategory.cs index 01929878..06727020 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/HighestRatedCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/HighestRatedCategory.cs @@ -1,43 +1,27 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class HighestRatedCategory : Category +public class HighestRatedCategory : SlotCategory { public override string Name { get; set; } = "Highest Rated"; public override string Description { get; set; } = "Community Highest Rated content"; public override string IconHash { get; set; } = "g820603"; public override string Endpoint { get; set; } = "thumbs"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => - database.Slots.Where(s => s.Type == SlotType.User && !s.CrossControllerRequired) - .Select(s => new SlotMetadata - { - Slot = s, - ThumbsUp = database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1), - }) - .OrderByDescending(s => s.ThumbsUp) - .Select(s => s.Slot) - .FirstOrDefault(); + public override string Tag => "highest_rated"; - public override IEnumerable GetSlots(DatabaseContext database, int pageStart, int pageSize) => - database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .Where(s => !s.CrossControllerRequired) - .Select(s => new SlotMetadata + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Select(s => new SlotMetadata { Slot = s, ThumbsUp = database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1), }) .OrderByDescending(s => s.ThumbsUp) - .ThenBy(_ => EF.Functions.Random()) .Select(s => s.Slot) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.Type == SlotType.User); + .Where(queryBuilder.Build()); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/LuckyDipCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/LuckyDipCategory.cs index 72ad0b95..49c57402 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/LuckyDipCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/LuckyDipCategory.cs @@ -1,26 +1,22 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class LuckyDipCategory : Category +public class LuckyDipCategory : SlotCategory { public override string Name { get; set; } = "Lucky Dip"; public override string Description { get; set; } = "A random selection of content"; public override string IconHash { get; set; } = "g820605"; - public override string Endpoint { get; set; } = "lbp2luckydip"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => database.Slots.Where(s => s.Type == SlotType.User && !s.CrossControllerRequired).OrderByDescending(_ => EF.Functions.Random()).FirstOrDefault(); - public override IQueryable GetSlots - (DatabaseContext database, int pageStart, int pageSize) - => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .Where(s => !s.CrossControllerRequired) - .OrderByDescending(_ => EF.Functions.Random()) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.Type == SlotType.User); + public override string Endpoint { get; set; } = "lucky_dip"; + public override string Tag => "lucky_dip"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Where(queryBuilder.Build()) + .ApplyOrdering(new SlotSortBuilder().AddSort(new FirstUploadedSort())); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/MostHeartedCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/MostHeartedCategory.cs index ce14ed0c..c06ba54d 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/MostHeartedCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/MostHeartedCategory.cs @@ -1,42 +1,29 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Misc; -using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class MostHeartedCategory : Category +public class MostHeartedCategory : SlotCategory { public override string Name { get; set; } = "Most Hearted"; public override string Description { get; set; } = "The Most Hearted Content"; public override string IconHash { get; set; } = "g820607"; - public override string Endpoint { get; set; } = "mostHearted"; + public override string Endpoint { get; set; } = "most_hearted"; + public override string Tag => "most_hearted"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => - database.Slots.Where(s => s.Type == SlotType.User && !s.CrossControllerRequired) - .Select(s => new SlotMetadata + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Select(s => new SlotMetadata { Slot = s, Hearts = database.HeartedLevels.Count(r => r.SlotId == s.SlotId), }) - .OrderByDescending(s => s.Hearts) + .ApplyOrdering(new SlotSortBuilder().AddSort(new HeartsSort())) .Select(s => s.Slot) - .FirstOrDefault(); - - public override IEnumerable GetSlots(DatabaseContext database, int pageStart, int pageSize) => - database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .Where(s => !s.CrossControllerRequired) - .Select(s => new SlotMetadata - { - Slot = s, - Hearts = database.HeartedLevels.Count(r => r.SlotId == s.SlotId), - }) - .OrderByDescending(s => s.Hearts) - .Select(s => s.Slot) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.Type == SlotType.User); + .Where(queryBuilder.Build()); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/MostPlayedCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/MostPlayedCategory.cs index 56e0d63a..68e725ee 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/MostPlayedCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/MostPlayedCategory.cs @@ -1,30 +1,21 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class MostPlayedCategory : Category +public class MostPlayedCategory : SlotCategory { public override string Name { get; set; } = "Most Played"; public override string Description { get; set; } = "The most played content"; public override string IconHash { get; set; } = "g820608"; - public override string Endpoint { get; set; } = "mostUniquePlays"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => database.Slots - .Where(s => s.Type == SlotType.User && !s.CrossControllerRequired) - .OrderByDescending(s => s.PlaysLBP1Unique + s.PlaysLBP2Unique + s.PlaysLBP3Unique) - .ThenByDescending(s => s.PlaysLBP1 + s.PlaysLBP2 + s.PlaysLBP3) - .FirstOrDefault(); - public override IQueryable GetSlots - (DatabaseContext database, int pageStart, int pageSize) - => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .Where(s => !s.CrossControllerRequired) + public override string Endpoint { get; set; } = "most_played"; + public override string Tag => "most_played"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Where(queryBuilder.Build()) .OrderByDescending(s => s.PlaysLBP1Unique + s.PlaysLBP2Unique + s.PlaysLBP3Unique) - .ThenByDescending(s => s.PlaysLBP1 + s.PlaysLBP2 + s.PlaysLBP3) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.Type == SlotType.User); + .ThenByDescending(s => s.PlaysLBP1 + s.PlaysLBP2 + s.PlaysLBP3); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/MyHeartedCreatorsCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/MyHeartedCreatorsCategory.cs new file mode 100644 index 00000000..09e6b386 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/MyHeartedCreatorsCategory.cs @@ -0,0 +1,21 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; + +public class MyHeartedCreatorsCategory : UserCategory +{ + public override string Name { get; set; } = "My Hearted Creators"; + public override string Description { get; set; } = "Creators you've hearted"; + public override string IconHash { get; set; } = "g820612"; + public override string Endpoint { get; set; } = "favourite_creators"; + public override string Tag => "favourite_creators"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token) => + database.HeartedProfiles.Where(h => h.UserId == token.UserId) + .OrderByDescending(h => h.UserId) + .Include(h => h.HeartedUser) + .Select(h => h.HeartedUser); +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs index 9656a4e9..3c86d566 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs @@ -1,25 +1,22 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class NewestLevelsCategory : Category +public class NewestLevelsCategory : SlotCategory { public override string Name { get; set; } = "Newest Levels"; public override string Description { get; set; } = "The most recently published content"; public override string IconHash { get; set; } = "g820623"; public override string Endpoint { get; set; } = "newest"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => database.Slots.Where(s => s.Type == SlotType.User && !s.CrossControllerRequired).OrderByDescending(s => s.FirstUploaded).FirstOrDefault(); - public override IQueryable GetSlots - (DatabaseContext database, int pageStart, int pageSize) - => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .Where(s => !s.CrossControllerRequired) - .OrderByDescending(s => s.FirstUploaded) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.Type == SlotType.User); + public override string Tag => "newest"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Where(queryBuilder.Build()) + .ApplyOrdering(new SlotSortBuilder().AddSort(new FirstUploadedSort())); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs new file mode 100644 index 00000000..00ed79ce --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/PlaylistCategory.cs @@ -0,0 +1,27 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; + +public abstract class PlaylistCategory : Category +{ + public override string[] Types { get; } = { "playlist", }; + + public abstract IQueryable GetItems(DatabaseContext database, GameTokenEntity token); + + public override async Task Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1) + { + List playlists = + (await this.GetItems(database, token).Take(numResults).ToListAsync()) + .ToSerializableList(GamePlaylist.CreateFromEntity); + + int totalPlaylists = await this.GetItems(database, token).CountAsync(); + return GameCategory.CreateFromEntity(this, new GenericSerializableList(playlists, totalPlaylists, numResults + 1)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/QueueCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/QueueCategory.cs index 265bc876..28c5e271 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/QueueCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/QueueCategory.cs @@ -1,38 +1,22 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class QueueCategory : CategoryWithUser +public class QueueCategory : SlotCategory { public override string Name { get; set; } = "My Queue"; public override string Description { get; set; } = "Your queued content"; public override string IconHash { get; set; } = "g820614"; public override string Endpoint { get; set; } = "queue"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database, UserEntity user) - => database.QueuedLevels.Where(q => q.UserId == user.UserId) - .Where(q => q.Slot.Type == SlotType.User && !q.Slot.Hidden && q.Slot.GameVersion <= GameVersion.LittleBigPlanet3) - .OrderByDescending(q => q.QueuedLevelId) - .Include(q => q.Slot.Creator) - .Select(q => q.Slot) - .ByGameVersion(GameVersion.LittleBigPlanet3, false, false, true) - .FirstOrDefault(); + public override string Tag => "my_queue"; - public override IQueryable GetSlots(DatabaseContext database, UserEntity user, int pageStart, int pageSize) - => database.QueuedLevels.Where(q => q.UserId == user.UserId) - .Where(q => q.Slot.Type == SlotType.User && !q.Slot.Hidden && q.Slot.GameVersion <= GameVersion.LittleBigPlanet3) + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.QueuedLevels.Where(q => q.UserId == token.UserId) .OrderByDescending(q => q.QueuedLevelId) - .Include(q => q.Slot.Creator) .Select(q => q.Slot) - .ByGameVersion(GameVersion.LittleBigPlanet3, false, false, true) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - - public override int GetTotalSlots(DatabaseContext database, UserEntity user) => database.QueuedLevels.Count(q => q.UserId == user.UserId); + .Where(queryBuilder.Build()); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs new file mode 100644 index 00000000..39d6986d --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/SlotCategory.cs @@ -0,0 +1,27 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; + +public abstract class SlotCategory : Category +{ + public override string[] Types { get; } = { "slot", "adventure", }; + + public abstract IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder); + + public override async Task Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1) + { + List slots = + (await this.GetItems(database, token, queryBuilder).Take(numResults).ToListAsync()) + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + + int totalSlots = await this.GetItems(database, token, queryBuilder).CountAsync(); + return GameCategory.CreateFromEntity(this, new GenericSerializableList(slots, totalSlots, numResults+1)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs index 3d5422f3..4386bb17 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/TeamPicksCategory.cs @@ -1,25 +1,23 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Filter.Sorts; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; -public class TeamPicksCategory : Category +public class TeamPicksCategory : SlotCategory { public override string Name { get; set; } = "Team Picks"; public override string Description { get; set; } = "Community Team Picks"; public override string IconHash { get; set; } = "g820626"; public override string Endpoint { get; set; } = "team_picks"; - public override SlotEntity? GetPreviewSlot(DatabaseContext database) => database.Slots.OrderByDescending(s => s.FirstUploaded).FirstOrDefault(s => s.TeamPick && !s.CrossControllerRequired); - public override IQueryable GetSlots - (DatabaseContext database, int pageStart, int pageSize) - => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) - .OrderByDescending(s => s.FirstUploaded) - .Where(s => s.TeamPick && !s.CrossControllerRequired) - .Skip(Math.Max(0, pageStart - 1)) - .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(DatabaseContext database) => database.Slots.Count(s => s.TeamPick); + public override string Tag => "team_picks"; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) => + database.Slots.Where(queryBuilder.Clone().AddFilter(new TeamPickFilter()).Build()) + .ApplyOrdering(new SlotSortBuilder().AddSort(new FirstUploadedSort())); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs new file mode 100644 index 00000000..d9826a92 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/UserCategory.cs @@ -0,0 +1,27 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; + +public abstract class UserCategory : Category +{ + public override string[] Types { get; } = { "user", }; + + public abstract IQueryable GetItems(DatabaseContext database, GameTokenEntity token); + + public override async Task Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1) + { + List users = + (await this.GetItems(database, token).Take(numResults).ToListAsync()) + .ToSerializableList(GameUser.CreateFromEntity); + + int totalUsers = await this.GetItems(database, token).CountAsync(); + return GameCategory.CreateFromEntity(this, new GenericSerializableList(users, totalUsers, numResults + 1)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Types/MyPlaylistsCategory.cs b/ProjectLighthouse.Servers.GameServer/Types/MyPlaylistsCategory.cs new file mode 100644 index 00000000..db80abe0 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/MyPlaylistsCategory.cs @@ -0,0 +1,19 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types; + +public class MyPlaylistsCategory : PlaylistCategory +{ + public override string Name { get; set; } = "My Playlists"; + public override string Description { get; set; } = "Your playlists"; + public override string IconHash { get; set; } = "g820613"; + public override string Endpoint { get; set; } = "my_playlists"; + public override string Tag => "my_playlists"; + public override string[] Types { get; } = { "playlist", }; + + public override IQueryable GetItems(DatabaseContext database, GameTokenEntity token) => + database.Playlists.Where(p => p.CreatorId == token.UserId).OrderByDescending(p => p.PlaylistId); +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminPanelPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminPanelPage.cshtml.cs index ed2456d1..92bff838 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminPanelPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminPanelPage.cshtml.cs @@ -2,6 +2,7 @@ using LBPUnion.ProjectLighthouse.Administration.Maintenance; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Types; @@ -29,7 +30,7 @@ public class AdminPanelPage : BaseLayout if (!user.IsAdmin) return this.NotFound(); this.Statistics.Add(new AdminPanelStatistic("Users", await StatisticsHelper.UserCount(this.Database), "/admin/users")); - this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount(this.Database))); + this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount(this.Database, new SlotQueryBuilder()))); this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount(this.Database))); this.Statistics.Add(new AdminPanelStatistic("API Keys", await StatisticsHelper.ApiKeyCount(this.Database), "/admin/keys")); diff --git a/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs new file mode 100644 index 00000000..eacce233 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotFilterTests.cs @@ -0,0 +1,234 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Serialization; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Users; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Integration; + +[Trait("Category", "Integration")] +public class SlotFilterTests : LighthouseServerTest +{ + [Fact] + public async Task GetUserSlot_ShouldReturnOk_WhenSlotExists() + { + DatabaseContext db = await IntegrationHelper.GetIntegrationDatabase(); + + db.Users.Add(new UserEntity + { + UserId = 23, + }); + + db.Slots.Add(new SlotEntity + { + SlotId = 23, + CreatorId = 23, + }); + await db.SaveChangesAsync(); + + LoginResult loginResult = await this.Authenticate(); + HttpResponseMessage response = await this.AuthenticatedRequest("/LITTLEBIGPLANETPS3_XML/s/user/23", loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + + Assert.Contains("23", body); + } + + [Fact] + public async Task NewestSlots_ShouldReturnSlotsOrderedByTimestampDescending() + { + DatabaseContext db = await IntegrationHelper.GetIntegrationDatabase(); + + for (int i = 1; i <= 100; i++) + { + db.Users.Add(new UserEntity + { + UserId = i, + Username = $"user{i}", + }); + db.Slots.Add(new SlotEntity + { + SlotId = i, + CreatorId = i, + FirstUploaded = i, + }); + } + await db.SaveChangesAsync(); + + LoginResult loginResult = await this.Authenticate(); + HttpResponseMessage response = + await this.AuthenticatedRequest("/LITTLEBIGPLANETPS3_XML/slots?pageStart=0&pageSize=10", loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + + object? deserialized = LighthouseSerializer + .GetSerializer(typeof(GameUserSlotList), new XmlRootAttribute("slots")) + .Deserialize(new StringReader(body)); + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + + GameUserSlotList slotResponse = (GameUserSlotList)deserialized; + + Assert.Equal(100, slotResponse.Total); + Assert.Equal(10, slotResponse.Slots.Count); + + Assert.Equal(91, slotResponse.Slots[9].FirstUploaded); + } + + [Fact] + public async Task NewestSlots_ShouldReturnSlotsWithAuthorLabel() + { + DatabaseContext db = await IntegrationHelper.GetIntegrationDatabase(); + + db.Users.Add(new UserEntity() + { + UserId = 1, + }); + + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 1, + AuthorLabels = "LABEL_SinglePlayer,LABEL_Quick,LABEL_Funny", + FirstUploaded = 1, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 2, + AuthorLabels = "LABEL_SinglePlayer", + FirstUploaded = 2, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 3, + AuthorLabels = "LABEL_Quick", + FirstUploaded = 3, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 4, + AuthorLabels = "LABEL_Funny", + FirstUploaded = 4, + }); + + await db.SaveChangesAsync(); + + LoginResult loginResult = await this.Authenticate(); + HttpResponseMessage response = + await this.AuthenticatedRequest("/LITTLEBIGPLANETPS3_XML/slots?pageStart=0&pageSize=10&labelFilter0=LABEL_Funny", + loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + + object? deserialized = LighthouseSerializer + .GetSerializer(typeof(GameUserSlotList), new XmlRootAttribute("slots")) + .Deserialize(new StringReader(body)); + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + + GameUserSlotList slotResponse = (GameUserSlotList)deserialized; + + const int expectedCount = 2; + + Assert.Equal(expectedCount, slotResponse.Slots.Count); + Assert.True(slotResponse.Slots.TrueForAll(s => s.AuthorLabels.Contains("LABEL_Funny"))); + } + +[Fact] + public async Task NewestSlots_ShouldReturnEmpty_WhenAuthorLabelsDontMatch() + { + DatabaseContext db = await IntegrationHelper.GetIntegrationDatabase(); + + db.Users.Add(new UserEntity + { + UserId = 1, + }); + + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 1, + AuthorLabels = "LABEL_SinglePlayer,LABEL_Quick,LABEL_Funny", + FirstUploaded = 1, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 2, + AuthorLabels = "LABEL_SinglePlayer", + FirstUploaded = 2, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 3, + AuthorLabels = "LABEL_Quick", + FirstUploaded = 3, + }); + db.Slots.Add(new SlotEntity + { + CreatorId = 1, + SlotId = 4, + AuthorLabels = "LABEL_Funny", + FirstUploaded = 4, + }); + + await db.SaveChangesAsync(); + + LoginResult loginResult = await this.Authenticate(); + HttpResponseMessage response = + await this.AuthenticatedRequest("/LITTLEBIGPLANETPS3_XML/slots?pageStart=0&pageSize=10&labelFilter0=LABEL_Funny&labelFilter1=LABEL_Quick&labelFilter2=LABEL_Gallery", + loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + + object? deserialized = LighthouseSerializer + .GetSerializer(typeof(GameUserSlotList), new XmlRootAttribute("slots")) + .Deserialize(new StringReader(body)); + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + + GameUserSlotList slotResponse = (GameUserSlotList)deserialized; + + Assert.Empty(slotResponse.Slots); + } + + [XmlRoot("slots")] + public class GameUserSlotList + { + [XmlElement("slot")] + public List Slots { get; set; } = new(); + + [XmlAttribute("total")] + public int Total { get; set; } + + [XmlAttribute("hint_start")] + public int HintStart { get; set; } + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs new file mode 100644 index 00000000..acb47cf4 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ControllerExtensionTests.cs @@ -0,0 +1,388 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class ControllerExtensionTests +{ + [Fact] + public void GetDefaultFilters_ShouldReturnFilterBuilder() + { + SlotQueryBuilder queryBuilder = new SlotsController(null!).GetDefaultFilters(MockHelper.GetUnitTestToken()); + + Assert.NotEmpty(queryBuilder.GetFilters(typeof(GameVersionFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(SubLevelFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(HiddenSlotFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(SlotTypeFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddExcludeLbp1Filter_WhenTokenNotLbp1() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString(), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ExcludeLBP1OnlyFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldReturnFilters_WhenQueryEmpty() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString(), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + + Assert.NotEmpty(queryBuilder.GetFilters(typeof(GameVersionFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ExcludeLBP1OnlyFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(SubLevelFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(HiddenSlotFilter))); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(SlotTypeFilter))); + } + + private static List GetDefaultFilters + (SlotQueryBuilder queryBuilder) => + queryBuilder.GetFilters(typeof(GameVersionFilter)) + .Union(queryBuilder.GetFilters(typeof(SubLevelFilter)) + .Union(queryBuilder.GetFilters(typeof(HiddenSlotFilter)) + .Union(queryBuilder.GetFilters(typeof(SlotTypeFilter))))) + .ToList(); + + [Fact] + public void FilterFromRequest_ShouldAddLabelFilter_WhenAuthorLabelPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?labelFilter0=LABEL_TEST"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(AuthorLabelFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddPlayerCountFilter_WhenPlayersPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?players=1"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(PlayerCountFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddTextFilter_WhenTextFilterPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?textFilter=test"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(TextFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddFirstUploadedFilter_WhenDateFilterPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?dateFilterType=thisWeek"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(FirstUploadedFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddExcludeMoveFilter_WhenMoveEqualsFalse() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?move=false"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ExcludeMovePackFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddMoveFilter_WhenMoveEqualsOnly() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?move=only"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(MovePackFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddCrossControlFilter_WhenCrossControlEqualsTrue() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?crosscontrol=true"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(CrossControlFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddAdventureFilter_WhenAdventureEqualsAllMust() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?adventure=allMust"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(AdventureFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddExcludeAdventureFilter_WhenAdventureEqualsNoneCan() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?adventure=noneCan"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ExcludeAdventureFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddMovePackFilter_WhenMoveEqualsAllMust() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?move=allMust"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(MovePackFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddExcludeMoveFilter_WhenMoveEqualsNoneCan() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?move=noneCan"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ExcludeMovePackFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddGameVersionListFilter_WhenGameFilterIsPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?gameFilter[]=lbp1&gameFilter[]=lbp3"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(GameVersionListFilter))); + } + + [Fact] + public void FilterFromRequest_ShouldAddResultTypeFilter_WhenResultTypeIsPresent() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + SlotsController controller = new(null!) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext + { + Request = + { + QueryString = new QueryString("?resultType[]=slot&resultType[]=playlist"), + }, + }, + }, + }; + + SlotQueryBuilder queryBuilder = controller.FilterFromRequest(token); + Assert.NotEmpty(queryBuilder.GetFilters(typeof(ResultTypeFilter))); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs index a6ad5cf5..8728a613 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs @@ -41,11 +41,8 @@ along with this program. If not, see ." + "\n"; IActionResult result = messageController.Eula(); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expected, (string)okObjectResult.Value); + string eulaMsg = result.CastTo(); + Assert.Equal(expected, eulaMsg); } [Fact] @@ -72,11 +69,8 @@ along with this program. If not, see ." + "\nuni IActionResult result = messageController.Eula(); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expected, (string)okObjectResult.Value); + string eulaMsg = result.CastTo(); + Assert.Equal(expected, eulaMsg); } [Fact] @@ -92,11 +86,8 @@ along with this program. If not, see ." + "\nuni IActionResult result = await messageController.Announce(); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expected, (string)okObjectResult.Value); + string announceMsg = result.CastTo(); + Assert.Equal(expected, announceMsg); } [Fact] @@ -112,11 +103,8 @@ along with this program. If not, see ." + "\nuni IActionResult result = await messageController.Announce(); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expected, (string)okObjectResult.Value); + string announceMsg = result.CastTo(); + Assert.Equal(expected, announceMsg); } [Fact] @@ -147,11 +135,8 @@ along with this program. If not, see ." + "\nuni IActionResult result = await messageController.Filter(new NullMailService()); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expectedBody, (string)okObjectResult.Value); + string filteredMessage = result.CastTo(); + Assert.Equal(expectedBody, filteredMessage); } [Fact] @@ -173,11 +158,8 @@ along with this program. If not, see ." + "\nuni IActionResult result = await messageController.Filter(new NullMailService()); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.NotNull(okObjectResult.Value); - Assert.Equal(expectedBody, (string)okObjectResult.Value); + string filteredMessage = result.CastTo(); + Assert.Equal(expectedBody, filteredMessage); } private static Mock getMailServiceMock() @@ -189,7 +171,7 @@ along with this program. If not, see ." + "\nuni } [Fact] - public async void Filter_ShouldNotSendEmail_WhenMailDisabled() + public async Task Filter_ShouldNotSendEmail_WhenMailDisabled() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); Mock mailMock = getMailServiceMock(); @@ -200,20 +182,16 @@ along with this program. If not, see ." + "\nuni ServerConfiguration.Instance.Mail.MailEnabled = false; CensorConfiguration.Instance.FilteredWordList = new List(); - const int expectedStatus = 200; const string expected = "/setemail unittest@unittest.com"; IActionResult result = await messageController.Filter(mailMock.Object); - Assert.IsType(result); - OkObjectResult? okObjectResult = result as OkObjectResult; - Assert.NotNull(okObjectResult); - Assert.Equal(expectedStatus, okObjectResult.StatusCode); - Assert.Equal(expected, okObjectResult.Value); + string filteredMessage = result.CastTo(); + Assert.Equal(expected, filteredMessage); } [Fact] - public async void Filter_ShouldSendEmail_WhenMailEnabled_AndEmailNotTaken() + public async Task Filter_ShouldSendEmail_WhenMailEnabled_AndEmailNotTaken() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -236,7 +214,7 @@ along with this program. If not, see ." + "\nuni } [Fact] - public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken() + public async Task Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken() { List users = new() { @@ -265,7 +243,7 @@ along with this program. If not, see ." + "\nuni } [Fact] - public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailAlreadyVerified() + public async Task Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailAlreadyVerified() { UserEntity unitTestUser = MockHelper.GetUnitTestUser(); unitTestUser.EmailAddressVerified = true; @@ -290,7 +268,7 @@ along with this program. If not, see ." + "\nuni } [Fact] - public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailFormatInvalid() + public async Task Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailFormatInvalid() { UserEntity unitTestUser = MockHelper.GetUnitTestUser(); unitTestUser.EmailAddressVerified = true; diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs new file mode 100644 index 00000000..da500021 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/SlotControllerTests.cs @@ -0,0 +1,380 @@ +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class SlotControllerTests +{ + #region SlotsBy + [Fact] + public async Task SlotsBy_ShouldReturnNotFound_WhenUserInvalid() + { + DatabaseContext db = await MockHelper.GetTestDatabase(); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.SlotsBy("bytest"); + + Assert.IsType(result); + } + + [Fact] + public async Task SlotsBy_ShouldFetchLevelsByUser() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + CreatorId = 2, + }, + new SlotEntity + { + SlotId = 2, + CreatorId = 2, + }, + new SlotEntity + { + SlotId = 3, + CreatorId = 3, + }, + }; + List users = new() + { + MockHelper.GetUnitTestUser(), + new UserEntity + { + Username = "bytest", + UserId = 2, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new IList[] + { + slots, users, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.SlotsBy("bytest"); + + const int expectedElements = 2; + + GenericSlotResponse slotResponse = result.CastTo(); + Assert.Equal(expectedElements, slotResponse.Slots.Count); + } + + [Fact] + public async Task SlotsBy_ResultsAreOrderedByFirstUploadedTimestampDescending() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + CreatorId = 2, + FirstUploaded = 3, + }, + new SlotEntity + { + SlotId = 2, + CreatorId = 2, + FirstUploaded = 1, + }, + new SlotEntity + { + SlotId = 3, + CreatorId = 2, + FirstUploaded = 2, + }, + }; + List users = new() + { + MockHelper.GetUnitTestUser(), + new UserEntity + { + Username = "bytest", + UserId = 2, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new IList[] + { + slots, users, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.SlotsBy("bytest"); + + const int expectedElements = 3; + const int expectedFirstSlotId = 1; + const int expectedSecondSlotId = 3; + const int expectedThirdSlotId = 2; + + GenericSlotResponse slotResponse = result.CastTo(); + Assert.Equal(expectedElements, slotResponse.Slots.Count); + + Assert.Equal(expectedFirstSlotId, ((GameUserSlot)slotResponse.Slots[0]).SlotId); + Assert.Equal(expectedSecondSlotId, ((GameUserSlot)slotResponse.Slots[1]).SlotId); + Assert.Equal(expectedThirdSlotId, ((GameUserSlot)slotResponse.Slots[2]).SlotId); + } + #endregion + + #region UserSlot + [Fact] + public async Task UserSlot_ShouldFetch_WhenSlotIsValid() + { + List slots = new() + { + new SlotEntity + { + SlotId = 2, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(2); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldNotFetch_WhenGameVersionMismatch() + { + List slots = new() + { + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanet2, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(2); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldFetch_WhenGameVersionEqual() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanetVita; + List tokens = new() + { + token, + }; + List slots = new() + { + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanetVita, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new IList[] + { + slots, tokens, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(token); + + IActionResult result = await slotsController.UserSlot(2); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldFetch_WhenGameVersionIsGreater() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + List tokens = new() + { + token, + }; + List slots = new() + { + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanet1, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new IList[] + { + slots, tokens, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(token); + + IActionResult result = await slotsController.UserSlot(2); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldReturnNotFound_WhenSlotDoesNotExist() + { + DatabaseContext db = await MockHelper.GetTestDatabase(); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(20); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldFetch_WhenSlotIsNotSubLevel() + { + List slots = new() + { + new SlotEntity + { + SlotId = 27, + CreatorId = 4, + SubLevel = false, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(27); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldNotFetch_WhenSlotIsHidden() + { + List slots = new() + { + new SlotEntity + { + SlotId = 27, + CreatorId = 4, + Hidden = true, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(27); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldNotFetch_WhenSlotIsWrongType() + { + List slots = new() + { + new SlotEntity + { + SlotId = 27, + Type = SlotType.Developer, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(27); + + Assert.IsType(result); + } + + [Fact] + public async Task UserSlot_ShouldNotFetch_WhenSlotIsSubLevel() + { + List slots = new() + { + new SlotEntity + { + SlotId = 27, + CreatorId = 4, + SubLevel = true, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new []{slots,}); + SlotsController slotsController = new(db); + slotsController.SetupTestController(); + + IActionResult result = await slotsController.UserSlot(27); + + Assert.IsType(result); + } + #endregion + + #region DeveloperSlot + [Fact] + public async Task DeveloperSlot_ShouldFetch_WhenSlotIdIsValid() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + InternalSlotId = 25, + Type = SlotType.Developer, + }, + }; + DatabaseContext db = await MockHelper.GetTestDatabase(new[] + { + slots, + }); + SlotsController controller = new(db); + controller.SetupTestController(); + + IActionResult result = await controller.DeveloperSlot(25); + Assert.IsType(result); + } + + [Fact] + public async Task DeveloperSlot_ShouldFetch_WhenSlotIdIsInvalid() + { + DatabaseContext db = await MockHelper.GetTestDatabase(); + SlotsController controller = new(db); + controller.SetupTestController(); + + IActionResult result = await controller.DeveloperSlot(26); + Assert.IsType(result); + } + #endregion + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs index 825643f1..c8da9fb7 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; @@ -14,7 +15,7 @@ namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; public class StatisticsControllerTests { [Fact] - public async void PlanetStats_ShouldReturnCorrectCounts_WhenEmpty() + public async Task PlanetStats_ShouldReturnCorrectCounts_WhenEmpty() { await using DatabaseContext db = await MockHelper.GetTestDatabase(); @@ -26,17 +27,13 @@ public class StatisticsControllerTests IActionResult result = await statsController.PlanetStats(); - Assert.IsType(result); - OkObjectResult? objectResult = result as OkObjectResult; - Assert.NotNull(objectResult); - PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; - Assert.NotNull(response); - Assert.Equal(expectedSlots, response.TotalSlotCount); - Assert.Equal(expectedTeamPicks, response.TeamPickCount); + PlanetStatsResponse statsResponse = result.CastTo(); + Assert.Equal(expectedSlots, statsResponse.TotalSlotCount); + Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount); } [Fact] - public async void PlanetStats_ShouldReturnCorrectCounts_WhenNotEmpty() + public async Task PlanetStats_ShouldReturnCorrectCounts_WhenNotEmpty() { List slots = new() { @@ -64,17 +61,13 @@ public class StatisticsControllerTests IActionResult result = await statsController.PlanetStats(); - Assert.IsType(result); - OkObjectResult? objectResult = result as OkObjectResult; - Assert.NotNull(objectResult); - PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; - Assert.NotNull(response); - Assert.Equal(expectedSlots, response.TotalSlotCount); - Assert.Equal(expectedTeamPicks, response.TeamPickCount); + PlanetStatsResponse statsResponse = result.CastTo(); + Assert.Equal(expectedSlots, statsResponse.TotalSlotCount); + Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount); } [Fact] - public async void PlanetStats_ShouldReturnCorrectCounts_WhenSlotsAreIncompatibleGameVersion() + public async Task PlanetStats_ShouldReturnCorrectCounts_WhenSlotsAreIncompatibleGameVersion() { List slots = new() { @@ -105,17 +98,13 @@ public class StatisticsControllerTests IActionResult result = await statsController.PlanetStats(); - Assert.IsType(result); - OkObjectResult? objectResult = result as OkObjectResult; - Assert.NotNull(objectResult); - PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; - Assert.NotNull(response); - Assert.Equal(expectedSlots, response.TotalSlotCount); - Assert.Equal(expectedTeamPicks, response.TeamPickCount); + PlanetStatsResponse statsResponse = result.CastTo(); + Assert.Equal(expectedSlots, statsResponse.TotalSlotCount); + Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount); } [Fact] - public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreCompatible() + public async Task TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreCompatible() { List slots = new() { @@ -145,14 +134,12 @@ public class StatisticsControllerTests IActionResult result = await statsController.TotalLevelCount(); - Assert.IsType(result); - OkObjectResult? objectResult = result as OkObjectResult; - Assert.NotNull(objectResult); - Assert.Equal(expectedTotal, objectResult.Value); + string totalSlotsResponse = result.CastTo(); + Assert.Equal(expectedTotal, totalSlotsResponse); } [Fact] - public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible() + public async Task TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible() { List slots = new() { @@ -178,15 +165,11 @@ public class StatisticsControllerTests StatisticsController statsController = new(dbMock); statsController.SetupTestController(); - const int expectedStatusCode = 200; const string expectedTotal = "0"; IActionResult result = await statsController.TotalLevelCount(); - Assert.IsType(result); - OkObjectResult? objectResult = result as OkObjectResult; - Assert.NotNull(objectResult); - Assert.Equal(expectedStatusCode, objectResult.StatusCode); - Assert.Equal(expectedTotal, objectResult.Value); + string totalSlots = result.CastTo(); + Assert.Equal(expectedTotal, totalSlots); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs index d46fecd5..e7228b2c 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; @@ -14,7 +15,7 @@ namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; public class UserControllerTests { [Fact] - public async void GetUser_WithValidUser_ShouldReturnUser() + public async Task GetUser_WithValidUser_ShouldReturnUser() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -25,16 +26,12 @@ public class UserControllerTests IActionResult result = await userController.GetUser("unittest"); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); - GameUser? gameUser = okObject.Value as GameUser; - Assert.NotNull(gameUser); + GameUser gameUser = result.CastTo(); Assert.Equal(expectedId, gameUser.UserId); } [Fact] - public async void GetUser_WithInvalidUser_ShouldReturnNotFound() + public async Task GetUser_WithInvalidUser_ShouldReturnNotFound() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -47,7 +44,7 @@ public class UserControllerTests } [Fact] - public async void GetUserAlt_WithInvalidUser_ShouldReturnEmptyList() + public async Task GetUserAlt_WithInvalidUser_ShouldReturnEmptyList() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -56,16 +53,12 @@ public class UserControllerTests IActionResult result = await userController.GetUserAlt(new[]{"notfound",}); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); - MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; - Assert.NotNull(userList); - Assert.Empty(userList.Value.Users); + MinimalUserListResponse userList = result.CastTo(); + Assert.Empty(userList.Users); } [Fact] - public async void GetUserAlt_WithOnlyInvalidUsers_ShouldReturnEmptyList() + public async Task GetUserAlt_WithOnlyInvalidUsers_ShouldReturnEmptyList() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -77,16 +70,12 @@ public class UserControllerTests "notfound", "notfound2", "notfound3", }); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); - MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; - Assert.NotNull(userList); - Assert.Empty(userList.Value.Users); + MinimalUserListResponse userList = result.CastTo(); + Assert.Empty(userList.Users); } [Fact] - public async void GetUserAlt_WithTwoInvalidUsers_AndOneValidUser_ShouldReturnOne() + public async Task GetUserAlt_WithTwoInvalidUsers_AndOneValidUser_ShouldReturnOne() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -99,16 +88,12 @@ public class UserControllerTests "notfound", "unittest", "notfound3", }); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); - MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; - Assert.NotNull(userList); - Assert.Single(userList.Value.Users); + MinimalUserListResponse userList = result.CastTo(); + Assert.Single(userList.Users); } [Fact] - public async void GetUserAlt_WithTwoValidUsers_ShouldReturnTwo() + public async Task GetUserAlt_WithTwoValidUsers_ShouldReturnTwo() { List users = new() { @@ -132,16 +117,12 @@ public class UserControllerTests "unittest2", "unittest", }); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); - MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; - Assert.NotNull(userList); - Assert.Equal(expectedLength, userList.Value.Users.Count); + MinimalUserListResponse userList = result.CastTo(); + Assert.Equal(expectedLength, userList.Users.Count); } [Fact] - public async void UpdateMyPins_ShouldReturnBadRequest_WhenBodyIsInvalid() + public async Task UpdateMyPins_ShouldReturnBadRequest_WhenBodyIsInvalid() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -155,7 +136,7 @@ public class UserControllerTests } [Fact] - public async void UpdateMyPins_ShouldUpdatePins() + public async Task UpdateMyPins_ShouldUpdatePins() { await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); @@ -167,15 +148,13 @@ public class UserControllerTests IActionResult result = await userController.UpdateMyPins(); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); + string pinsResponse = result.CastTo(); Assert.Equal(expectedPins, dbMock.Users.First().Pins); - Assert.Equal(expectedResponse, okObject.Value); + Assert.Equal(expectedResponse, pinsResponse); } [Fact] - public async void UpdateMyPins_ShouldNotSave_WhenPinsAreEqual() + public async Task UpdateMyPins_ShouldNotSave_WhenPinsAreEqual() { UserEntity entity = MockHelper.GetUnitTestUser(); entity.Pins = "1234"; @@ -193,10 +172,9 @@ public class UserControllerTests IActionResult result = await userController.UpdateMyPins(); - Assert.IsType(result); - OkObjectResult? okObject = result as OkObjectResult; - Assert.NotNull(okObject); + string pinsResponse = result.CastTo(); + Assert.Equal(expectedPins, dbMock.Users.First().Pins); - Assert.Equal(expectedResponse, okObject.Value); + Assert.Equal(expectedResponse, pinsResponse); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs index f5e705b5..c8c18249 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs @@ -16,7 +16,7 @@ public class DigestMiddlewareTests { [Fact] - public async void DigestMiddleware_ShouldNotComputeDigests_WhenDigestsDisabled() + public async Task DigestMiddleware_ShouldNotComputeDigests_WhenDigestsDisabled() { DefaultHttpContext context = new() { @@ -44,7 +44,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldReject_WhenDigestHeaderIsMissing() + public async Task DigestMiddleware_ShouldReject_WhenDigestHeaderIsMissing() { DefaultHttpContext context = new() { @@ -77,7 +77,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldReject_WhenRequestDigestInvalid() + public async Task DigestMiddleware_ShouldReject_WhenRequestDigestInvalid() { DefaultHttpContext context = new() { @@ -112,7 +112,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldUseAlternateDigest_WhenPrimaryDigestInvalid() + public async Task DigestMiddleware_ShouldUseAlternateDigest_WhenPrimaryDigestInvalid() { DefaultHttpContext context = new() { @@ -150,8 +150,8 @@ public class DigestMiddlewareTests Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); } -[Fact] - public async void DigestMiddleware_ShouldNotReject_WhenRequestingAnnounce() + [Fact] + public async Task DigestMiddleware_ShouldNotReject_WhenRequestingAnnounce() { DefaultHttpContext context = new() { @@ -188,7 +188,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldCalculate_WhenAuthCookieEmpty() + public async Task DigestMiddleware_ShouldCalculate_WhenAuthCookieEmpty() { DefaultHttpContext context = new() { @@ -225,7 +225,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldComputeDigestsWithNoBody_WhenDigestsEnabled() + public async Task DigestMiddleware_ShouldComputeDigestsWithNoBody_WhenDigestsEnabled() { DefaultHttpContext context = new() { @@ -263,7 +263,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndNoResponseBody() + public async Task DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndNoResponseBody() { DefaultHttpContext context = new() { @@ -301,7 +301,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndResponseBody() + public async Task DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndResponseBody() { DefaultHttpContext context = new() { @@ -339,7 +339,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenUploading() + public async Task DigestMiddleware_ShouldComputeDigestsWithBody_WhenUploading() { DefaultHttpContext context = new() { @@ -377,7 +377,7 @@ public class DigestMiddlewareTests } [Fact] - public async void DigestMiddleware_ShouldCompressResponse_WhenAcceptEncodingHeaderIsPresent() + public async Task DigestMiddleware_ShouldCompressResponse_WhenAcceptEncodingHeaderIsPresent() { DefaultHttpContext context = new() { diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs index fa57bc47..b1cd03f9 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs @@ -17,9 +17,8 @@ namespace ProjectLighthouse.Tests.GameApiTests.Unit.Middlewares; [Trait("Category", "Unit")] public class SetLastContactMiddlewareTests { - [Fact] - public async void SetLastContact_ShouldAddLastContact_WhenTokenIsLBP1() + public async Task SetLastContact_ShouldAddLastContact_WhenTokenIsLBP1() { DefaultHttpContext context = new() { @@ -56,7 +55,7 @@ public class SetLastContactMiddlewareTests } [Fact] - public async void SetLastContact_ShouldUpdateLastContact_WhenTokenIsLBP1() + public async Task SetLastContact_ShouldUpdateLastContact_WhenTokenIsLBP1() { DefaultHttpContext context = new() { @@ -106,7 +105,7 @@ public class SetLastContactMiddlewareTests } [Fact] - public async void SetLastContact_ShouldNotAddLastContact_WhenTokenIsNotLBP1() + public async Task SetLastContact_ShouldNotAddLastContact_WhenTokenIsNotLBP1() { DefaultHttpContext context = new() { @@ -146,5 +145,4 @@ public class SetLastContactMiddlewareTests LastContactEntity? lastContactEntity = dbMock.LastContacts.FirstOrDefault(); Assert.Null(lastContactEntity); } - } \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs b/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs index 99fb6683..2f570755 100644 --- a/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs +++ b/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs @@ -8,7 +8,6 @@ namespace LBPUnion.ProjectLighthouse.Tests.Helpers; public static class IntegrationHelper { - private static readonly Lazy dbConnected = new(IsDbConnected); private static bool IsDbConnected() => ServerStatics.DbConnected; diff --git a/ProjectLighthouse.Tests/Helpers/MockHelper.cs b/ProjectLighthouse.Tests/Helpers/MockHelper.cs index 5ea02505..efac1356 100644 --- a/ProjectLighthouse.Tests/Helpers/MockHelper.cs +++ b/ProjectLighthouse.Tests/Helpers/MockHelper.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.EntityFrameworkCore; +using Xunit; namespace LBPUnion.ProjectLighthouse.Tests.Helpers; @@ -38,6 +39,18 @@ public static class MockHelper UserToken = "unittest", }; + public static T2 CastTo(this IActionResult result) where T1 : ObjectResult + { + Assert.IsType(result); + T1? typedResult = result as T1; + Assert.NotNull(typedResult); + Assert.NotNull(typedResult.Value); + Assert.IsType(typedResult.Value); + T2? finalResult = (T2?)typedResult.Value; + Assert.NotNull(finalResult); + return finalResult; + } + public static async Task GetTestDatabase(IEnumerable sets, [CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0) { Dictionary setDict = new(); @@ -109,9 +122,14 @@ public static class MockHelper } public static void SetupTestController(this ControllerBase controllerBase, string? body = null) + { + SetupTestController(controllerBase, GetUnitTestToken(), body); + } + + public static void SetupTestController(this ControllerBase controllerBase, GameTokenEntity token, string? body = null) { controllerBase.ControllerContext = GetMockControllerContext(body); - SetupTestGameToken(controllerBase, GetUnitTestToken()); + SetupTestGameToken(controllerBase, token); } public static ControllerContext GetMockControllerContext() => diff --git a/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs b/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs index ed691388..5faa9ff9 100644 --- a/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs +++ b/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs @@ -88,6 +88,8 @@ public class LighthouseServerTest where TStartup : class private Task AuthenticatedRequest(string endpoint, string mmAuth, HttpMethod method) { + if (!endpoint.StartsWith("/")) endpoint = $"/{endpoint}"; + using HttpRequestMessage requestMessage = new(method, endpoint); requestMessage.Headers.Add("Cookie", mmAuth); string path = endpoint.Split("?", StringSplitOptions.RemoveEmptyEntries)[0]; diff --git a/ProjectLighthouse.Tests/Unit/FilterTests.cs b/ProjectLighthouse.Tests/Unit/FilterTests.cs new file mode 100644 index 00000000..e73d64dc --- /dev/null +++ b/ProjectLighthouse.Tests/Unit/FilterTests.cs @@ -0,0 +1,841 @@ +using System; +using System.Collections.Generic; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Filter.Filters; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Users; +using Xunit; + +namespace LBPUnion.ProjectLighthouse.Tests.Unit; + +[Trait("Category", "Unit")] +public class FilterTests +{ + [Fact] + public void QueryBuilder_DoesDeepClone() + { + SlotQueryBuilder queryBuilder = new(); + queryBuilder.AddFilter(new CrossControlFilter()); + + SlotQueryBuilder clonedBuilder = queryBuilder.Clone(); + + Assert.NotEqual(queryBuilder, clonedBuilder); + } + + [Fact] + public void AdventureFilter_ShouldAccept_WhenAdventure() + { + AdventureFilter adventureFilter = new(); + Func adventureFunc = adventureFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + IsAdventurePlanet = true, + }; + + Assert.True(adventureFunc(slot)); + } + + [Fact] + public void AdventureFilter_ShouldReject_WhenNotAdventure() + { + AdventureFilter adventureFilter = new(); + Func adventureFunc = adventureFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + IsAdventurePlanet = false, + }; + + Assert.False(adventureFunc(slot)); + } + + [Fact] + public void AuthorLabelFilter_ShouldAccept_WhenExactMatch() + { + string[] filters = + { + "LABEL_Test", "LABEL_Unit", + }; + AuthorLabelFilter labelFilter = new(filters); + Func labelFunc = labelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + AuthorLabels = "LABEL_Test,LABEL_Unit", + }; + + Assert.True(labelFunc(slot)); + } + + [Fact] + public void AuthorLabelFilter_ShouldAccept_WhenExactMatch_AndExtraLabelsPresent() + { + string[] filters = + { + "LABEL_Test", "LABEL_Unit", + }; + AuthorLabelFilter labelFilter = new(filters); + Func labelFunc = labelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + AuthorLabels = "LABEL_Test,LABEL_Unit,LABEL_Lighthouse,LABEL_Bruh", + }; + + Assert.True(labelFunc(slot)); + } + + [Fact] + public void AuthorLabelFilter_ShouldAccept_WhenFilterEmpty_AndLabelsEmpty() + { + string[] filters = Array.Empty(); + AuthorLabelFilter labelFilter = new(filters); + Func labelFunc = labelFilter.GetPredicate().Compile(); + + SlotEntity slotWithNoLabels = new() + { + AuthorLabels = "", + }; + + Assert.True(labelFunc(slotWithNoLabels)); + } + + [Fact] + public void AuthorLabelFilter_ShouldReject_WhenNoneMatch() + { + string[] filters = + { + "LABEL_Test", "LABEL_Unit", + }; + AuthorLabelFilter labelFilter = new(filters); + Func labelFunc = labelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + AuthorLabels = "LABEL_Adventure,LABEL_Versus", + }; + + Assert.False(labelFunc(slot)); + } + + [Fact] + public void CreatorFilter_ShouldAccept_WhenCreatorIdMatch() + { + const int creatorId = 27; + CreatorFilter creatorFilter = new(creatorId); + Func creatorFunc = creatorFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = creatorId, + }; + + Assert.True(creatorFunc(slot)); + } + + [Fact] + public void CreatorFilter_ShouldReject_WhenCreatorIdMismatch() + { + const int filterCreatorId = 27; + const int slotCreatorId = 28; + CreatorFilter creatorFilter = new(filterCreatorId); + Func creatorFunc = creatorFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = slotCreatorId, + }; + + Assert.False(creatorFunc(slot)); + } + + [Fact] + public void CrossControlFilter_ShouldAccept_WhenCrossControlRequired() + { + CrossControlFilter crossControlFilter = new(); + Func ccFunc = crossControlFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CrossControllerRequired = true, + }; + + Assert.True(ccFunc(slot)); + } + + [Fact] + public void CrossControlFilter_ShouldReject_WhenCrossControlNotRequired() + { + CrossControlFilter crossControlFilter = new(); + Func ccFunc = crossControlFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CrossControllerRequired = false, + }; + + Assert.False(ccFunc(slot)); + } + + [Fact] + public void ExcludeAdventureFilter_ShouldReject_WhenAdventure() + { + ExcludeAdventureFilter excludeAdventureFilter = new(); + Func adventureFunc = excludeAdventureFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + IsAdventurePlanet = true, + }; + + Assert.False(adventureFunc(slot)); + } + + [Fact] + public void ExcludeAdventureFilter_ShouldAccept_WhenNotAdventure() + { + ExcludeAdventureFilter excludeAdventureFilter = new(); + Func adventureFunc = excludeAdventureFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + IsAdventurePlanet = false, + }; + + Assert.True(adventureFunc(slot)); + } + + [Fact] + public void ExcludeLBP1OnlyFilter_ShouldReject_WhenLbp1Only_AndTokenNotLbp1_AndNotCreator() + { + ExcludeLBP1OnlyFilter excludeLBP1 = new(10, GameVersion.LittleBigPlanet2); + Func excludeFunc = excludeLBP1.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Lbp1Only = true, + }; + + Assert.False(excludeFunc(slot)); + } + + [Fact] + public void ExcludeLBP1OnlyFilter_ShouldAccept_WhenLbp1Only_AndTokenLbp1() + { + ExcludeLBP1OnlyFilter excludeLBP1 = new(10, GameVersion.LittleBigPlanet1); + Func excludeFunc = excludeLBP1.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Lbp1Only = true, + }; + + Assert.True(excludeFunc(slot)); + } + + [Fact] + public void ExcludeLBP1OnlyFilter_ShouldAccept_WhenLbp1Only_AndTokenNotLbp1_AndIsCreator() + { + ExcludeLBP1OnlyFilter excludeLBP1 = new(10, GameVersion.LittleBigPlanet2); + Func excludeFunc = excludeLBP1.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = 10, + Lbp1Only = true, + }; + + Assert.True(excludeFunc(slot)); + } + + [Fact] + public void ExcludeMovePackFilter_ShouldReject_WhenMoveRequired() + { + ExcludeMovePackFilter excludeMove = new(); + Func excludeFunc = excludeMove.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MoveRequired = true, + }; + + Assert.False(excludeFunc(slot)); + } + + [Fact] + public void ExcludeMovePackFilter_ShouldAccept_WhenMoveNotRequired() + { + ExcludeMovePackFilter excludeMove = new(); + Func excludeFunc = excludeMove.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MoveRequired = false, + }; + + Assert.True(excludeFunc(slot)); + } + + [Fact] + public void FirstUploadedFilter_ShouldReject_WhenOlderThanStartTime() + { + FirstUploadedFilter uploadFilter = new(1000); + Func uploadFunc = uploadFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + FirstUploaded = 999, + }; + + Assert.False(uploadFunc(slot)); + } + + [Fact] + public void FirstUploadedFilter_ShouldAccept_WhenNewerThanStartTime() + { + FirstUploadedFilter uploadFilter = new(1000); + Func uploadFunc = uploadFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + FirstUploaded = 1001, + }; + + Assert.True(uploadFunc(slot)); + } + + [Fact] + public void FirstUploadedFilter_ShouldReject_WhenOlderThanEndTime() + { + FirstUploadedFilter uploadFilter = new(0, 1000); + Func uploadFunc = uploadFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + FirstUploaded = 1001, + }; + + Assert.False(uploadFunc(slot)); + } + + [Fact] + public void FirstUploadedFilter_ShouldAccept_WhenNewerThanEndTime() + { + FirstUploadedFilter uploadFilter = new(0, 1000); + Func uploadFunc = uploadFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + FirstUploaded = 999, + }; + + Assert.True(uploadFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldAccept_WhenExact_AndEqual() + { + GameVersionFilter gameVersionFilter = new(GameVersion.LittleBigPlanet1, true); + Func versionFunc = gameVersionFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet1, + }; + + Assert.True(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldReject_WhenExact_AndNotEqual() + { + GameVersionFilter gameVersionFilter = new(GameVersion.LittleBigPlanet2, true); + Func versionFunc = gameVersionFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet1, + }; + + Assert.False(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldAccept_WhenNotExact_AndGreaterThan() + { + GameVersionFilter gameVersionFilter = new(GameVersion.LittleBigPlanet2); + Func versionFunc = gameVersionFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet1, + }; + + Assert.True(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldAccept_WhenNotExact_AndEqual() + { + GameVersionFilter gameVersionFilter = new(GameVersion.LittleBigPlanet2); + Func versionFunc = gameVersionFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet2, + }; + + Assert.True(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldReject_WhenNotExact_AndLessThan() + { + GameVersionFilter gameVersionFilter = new(GameVersion.LittleBigPlanet1); + Func versionFunc = gameVersionFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet2, + }; + + Assert.False(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldReject_WhenVersionNotInList() + { + GameVersionListFilter gameVersionListFilter = new(GameVersion.LittleBigPlanet1, GameVersion.LittleBigPlanet2); + Func versionFunc = gameVersionListFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet3, + }; + + Assert.False(versionFunc(slot)); + } + + [Fact] + public void GameVersionFilter_ShouldAccept_WhenVersionIsInList() + { + GameVersionListFilter gameVersionListFilter = new(GameVersion.LittleBigPlanet1, GameVersion.LittleBigPlanet2); + Func versionFunc = gameVersionListFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + GameVersion = GameVersion.LittleBigPlanet1, + }; + + Assert.True(versionFunc(slot)); + } + + [Fact] + public void HiddenSlotFilter_ShouldReject_WhenHidden() + { + HiddenSlotFilter hiddenSlotFilter = new(); + Func hiddenFunc = hiddenSlotFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Hidden = true, + }; + + Assert.False(hiddenFunc(slot)); + } + + [Fact] + public void HiddenSlotFilter_ShouldAccept_WhenNotHidden() + { + HiddenSlotFilter hiddenSlotFilter = new(); + Func hiddenFunc = hiddenSlotFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Hidden = false, + }; + + Assert.True(hiddenFunc(slot)); + } + + [Fact] + public void MoveFilter_ShouldAccept_WhenMoveRequired() + { + MovePackFilter movePackFilter = new(); + Func moveFunc = movePackFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MoveRequired = true, + }; + + Assert.True(moveFunc(slot)); + } + + [Fact] + public void MoveFilter_ShouldReject_WhenMoveNotRequired() + { + MovePackFilter movePackFilter = new(); + Func moveFunc = movePackFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MoveRequired = false, + }; + + Assert.False(moveFunc(slot)); + } + + [Fact] + public void PlayerCountFilter_ShouldReject_WhenHigherThanMaxPlayers() + { + PlayerCountFilter playerCountFilter = new(maxPlayers: 2); + Func countFunc = playerCountFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MinimumPlayers = 1, + MaximumPlayers = 4, + }; + + Assert.False(countFunc(slot)); + } + + [Fact] + public void PlayerCountFilter_ShouldReject_WhenLowerThanMinPlayers() + { + PlayerCountFilter playerCountFilter = new(minPlayers: 2); + Func countFunc = playerCountFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MinimumPlayers = 1, + MaximumPlayers = 4, + }; + + Assert.False(countFunc(slot)); + } + + [Fact] + public void PlayerCountFilter_ShouldAccept_WhenLowerThanMaxPlayers() + { + PlayerCountFilter playerCountFilter = new(maxPlayers: 3); + Func countFunc = playerCountFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MinimumPlayers = 1, + MaximumPlayers = 2, + }; + + Assert.True(countFunc(slot)); + } + + [Fact] + public void PlayerCountFilter_ShouldAccept_WhenHigherThanMinPlayers() + { + PlayerCountFilter playerCountFilter = new(minPlayers: 2); + Func countFunc = playerCountFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + MinimumPlayers = 3, + MaximumPlayers = 4, + }; + + Assert.True(countFunc(slot)); + } + + [Fact] + public void ResultTypeFilter_ShouldReject_WhenSlotNotPresent() + { + ResultTypeFilter resultFilter = new(); + Func resultFunc = resultFilter.GetPredicate().Compile(); + + SlotEntity slot = new(); + + Assert.False(resultFunc(slot)); + } + + [Fact] + public void ResultTypeFilter_ShouldAccept_WhenSlotPresent() + { + ResultTypeFilter resultFilter = new("slot"); + Func resultFunc = resultFilter.GetPredicate().Compile(); + + SlotEntity slot = new(); + + Assert.True(resultFunc(slot)); + } + + [Fact] + public void SlotIdFilter_ShouldReject_WhenSlotIdNotPresent() + { + SlotIdFilter idFilter = new(new List + { + 2, + }); + Func idFunc = idFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + SlotId = 1, + }; + + Assert.False(idFunc(slot)); + } + + [Fact] + public void SlotIdFilter_ShouldAccept_WhenSlotIdPresent() + { + SlotIdFilter idFilter = new(new List + { + 2, + }); + Func idFunc = idFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + SlotId = 2, + }; + + Assert.True(idFunc(slot)); + } + + [Fact] + public void SlotTypeFilter_ShouldAccept_WhenSlotTypeMatches() + { + SlotTypeFilter slotTypeFilter = new(SlotType.User); + Func typeFunc = slotTypeFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Type = SlotType.User, + }; + + Assert.True(typeFunc(slot)); + } + + [Fact] + public void SlotTypeFilter_ShouldAccept_WhenSlotTypeDoesNotMatch() + { + SlotTypeFilter slotTypeFilter = new(SlotType.User); + Func typeFunc = slotTypeFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Type = SlotType.Developer, + }; + + Assert.False(typeFunc(slot)); + } + + [Fact] + public void SubLevelFilter_ShouldAccept_WhenUserIsCreator_AndNotSubLevel() + { + SubLevelFilter subLevelFilter = new(2); + Func subLevelFunc = subLevelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = 2, + SubLevel = false, + }; + + Assert.True(subLevelFunc(slot)); + } + + [Fact] + public void SubLevelFilter_ShouldAccept_WhenUserIsCreator_AndSubLevel() + { + SubLevelFilter subLevelFilter = new(2); + Func subLevelFunc = subLevelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = 2, + SubLevel = true, + }; + + Assert.True(subLevelFunc(slot)); + } + + [Fact] + public void SubLevelFilter_ShouldReject_WhenUserIsNotCreator_AndSubLevel() + { + SubLevelFilter subLevelFilter = new(2); + Func subLevelFunc = subLevelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = 1, + SubLevel = true, + }; + + Assert.False(subLevelFunc(slot)); + } + + [Fact] + public void SubLevelFilter_ShouldAccept_WhenUserIsNotCreator_AndNotSubLevel() + { + SubLevelFilter subLevelFilter = new(2); + Func subLevelFunc = subLevelFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + CreatorId = 1, + SubLevel = false, + }; + + Assert.True(subLevelFunc(slot)); + } + + [Fact] + public void TeamPickFilter_ShouldAccept_WhenTeamPick() + { + TeamPickFilter teamPickFilter = new(); + Func teamPickFunc = teamPickFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + TeamPick = true, + }; + + Assert.True(teamPickFunc(slot)); + } + + [Fact] + public void TeamPickFilter_ShouldReject_WhenNotTeamPick() + { + TeamPickFilter teamPickFilter = new(); + Func teamPickFunc = teamPickFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + TeamPick = false, + }; + + Assert.False(teamPickFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldAccept_WhenDescriptionContainsText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Description = "unit test", + }; + + Assert.True(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldReject_WhenDescriptionDoesNotContainText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Description = "fraction exam", + }; + + Assert.False(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldAccept_WhenNameContainsText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Name = "unit test", + }; + + Assert.True(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldReject_WhenNameDoesNotContainText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Name = "fraction exam", + }; + + Assert.False(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldAccept_WhenIdContainsText() + { + TextFilter textFilter = new("21"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + SlotId = 21, + }; + + Assert.True(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldReject_WhenIdDoesNotContainText() + { + TextFilter textFilter = new("21"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + SlotId = 19, + }; + + Assert.False(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldAccept_WhenCreatorUsernameContainsText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Creator = new UserEntity + { + Username = "test", + }, + }; + + Assert.True(textFunc(slot)); + } + + [Fact] + public void TextFilter_ShouldReject_WhenCreatorUsernameDoesNotContainText() + { + TextFilter textFilter = new("test"); + Func textFunc = textFilter.GetPredicate().Compile(); + + SlotEntity slot = new() + { + Creator = new UserEntity + { + Username = "bruh", + }, + }; + + Assert.False(textFunc(slot)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Unit/PaginationTests.cs b/ProjectLighthouse.Tests/Unit/PaginationTests.cs new file mode 100644 index 00000000..de591d94 --- /dev/null +++ b/ProjectLighthouse.Tests/Unit/PaginationTests.cs @@ -0,0 +1,238 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace LBPUnion.ProjectLighthouse.Tests.Unit; + +[Trait("Category", "Unit")] +public class PaginationTests +{ + [Fact] + public void GetPaginationData_IsReadFromQuery() + { + DefaultHttpContext defaultHttpContext = new() + { + Request = + { + Query = new QueryCollection(new Dictionary + { + { + "pageStart", new StringValues("10") + }, + { + "pageSize", new StringValues("15") + }, + }), + }, + }; + + PaginationData pageData = defaultHttpContext.Request.GetPaginationData(); + + const int expectedPageStart = 10; + const int expectedPageSize = 15; + + Assert.Equal(expectedPageStart, pageData.PageStart); + Assert.Equal(expectedPageSize, pageData.PageSize); + } + + [Fact] + public void GetPaginationData_IsPageStartSetToDefault_WhenMissing() + { + DefaultHttpContext defaultHttpContext = new() + { + Request = + { + Query = new QueryCollection(new Dictionary + { + { + "pageSize", new StringValues("15") + }, + }), + }, + }; + PaginationData pageData = defaultHttpContext.Request.GetPaginationData(); + + const int expectedPageStart = 0; + const int expectedPageSize = 15; + + Assert.Equal(expectedPageStart, pageData.PageStart); + Assert.Equal(expectedPageSize, pageData.PageSize); + } + + [Fact] + public void GetPaginationData_IsPageSizeSetToDefault_WhenMissing() + { + ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots = 50; + + DefaultHttpContext defaultHttpContext = new() + { + Request = + { + Query = new QueryCollection(new Dictionary + { + { + "pageStart", new StringValues("10") + }, + }), + }, + }; + PaginationData pageData = defaultHttpContext.Request.GetPaginationData(); + + const int expectedPageStart = 10; + const int expectedPageSize = 50; + + Assert.Equal(expectedPageStart, pageData.PageStart); + Assert.Equal(expectedPageSize, pageData.PageSize); + } + + [Fact] + public void GetPaginationData_NegativeValuesAreSetToZero() + { + ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots = 50; + + DefaultHttpContext defaultHttpContext = new() + { + Request = + { + Query = new QueryCollection(new Dictionary + { + { + "pageStart", new StringValues("-10") + }, + { + "pageSize", new StringValues("-10") + }, + }), + }, + }; + PaginationData pageData = defaultHttpContext.Request.GetPaginationData(); + + const int expectedPageStart = 0; + const int expectedPageSize = 10; + + Assert.Equal(expectedPageStart, pageData.PageStart); + Assert.Equal(expectedPageSize, pageData.PageSize); + } + + [Fact] + public void ApplyPagination_ShouldApplyCorrectPagination() + { + List users = new(); + for (int i = 0; i < 30; i++) + { + users.Add(new GameUser + { + UserId = i+1, + }); + } + + PaginationData pageData = new() + { + PageSize = 5, + PageStart = 6, + }; + List pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList(); + + Assert.Equal(pageData.PageSize, pagedUsers.Count); + Assert.Equal(6, pagedUsers[0].UserId); + Assert.Equal(10, pagedUsers[4].UserId); + } + + [Fact] + public void ApplyPagination_ShouldClampPageStart_WhenNegative() + { + List users = new(); + for (int i = 0; i < 30; i++) + { + users.Add(new GameUser + { + UserId = i + 1, + }); + } + + PaginationData pageData = new() + { + PageSize = 5, + PageStart = -5, + }; + List pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList(); + + Assert.Equal(pageData.PageSize, pagedUsers.Count); + Assert.Equal(1, pagedUsers[0].UserId); + Assert.Equal(5, pagedUsers[4].UserId); + } + + [Fact] + public void ApplyPagination_ShouldReturnEmpty_WhenPageSizeNegative() + { + List users = new(); + for (int i = 0; i < 30; i++) + { + users.Add(new GameUser + { + UserId = i + 1, + }); + } + + PaginationData pageData = new() + { + PageSize = -5, + PageStart = 0, + }; + List pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList(); + + Assert.Empty(pagedUsers); + } + + [Fact] + public void ApplyPagination_ShouldClampPageSize_WhenSizeExceedsMaxElements() + { + List users = new(); + for (int i = 0; i < 30; i++) + { + users.Add(new GameUser + { + UserId = i + 1, + }); + } + + PaginationData pageData = new() + { + PageSize = 10, + PageStart = 0, + MaxElements = 1, + }; + List pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList(); + + Assert.Single(pagedUsers); + } + + [Fact] + public void ApplyPagination_ShouldClampPageSize_WhenSizeExceedsInternalLimit() + { + List users = new(); + for (int i = 0; i < 1001; i++) + { + users.Add(new GameUser + { + UserId = i + 1, + }); + } + + PaginationData pageData = new() + { + PageSize = int.MaxValue, + PageStart = 0, + MaxElements = int.MaxValue, + }; + List pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList(); + + Assert.Equal(1000, pagedUsers.Count); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Unit/ResourceTests.cs b/ProjectLighthouse.Tests/Unit/ResourceTests.cs index 84d04918..3fe5a758 100644 --- a/ProjectLighthouse.Tests/Unit/ResourceTests.cs +++ b/ProjectLighthouse.Tests/Unit/ResourceTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Types.Resources; using Xunit; @@ -44,7 +45,7 @@ public class ResourceTests } [Fact] - public async void ShouldDeleteResourceAndImage() + public async Task ShouldDeleteResourceAndImage() { FileHelper.EnsureDirectoryCreated(FileHelper.ResourcePath); FileHelper.EnsureDirectoryCreated(FileHelper.ImagePath); diff --git a/ProjectLighthouse/Extensions/DatabaseExtensions.cs b/ProjectLighthouse/Extensions/DatabaseExtensions.cs index ef990b85..86a3ef3a 100644 --- a/ProjectLighthouse/Extensions/DatabaseExtensions.cs +++ b/ProjectLighthouse/Extensions/DatabaseExtensions.cs @@ -2,56 +2,12 @@ using System; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Extensions; public static class DatabaseExtensions { - public static IQueryable ByGameVersion - (this DbSet set, GameVersion gameVersion, bool includeSublevels = false, bool includeCreator = false) - => set.AsQueryable().ByGameVersion(gameVersion, includeSublevels, includeCreator); - - public static IQueryable ByGameVersion - (this IQueryable query, GameVersion gameVersion, bool includeSublevels = false, bool includeCreator = false, bool includeDeveloperLevels = false) - { - query = query.Where(s => s.Type == SlotType.User || (s.Type == SlotType.Developer && includeDeveloperLevels)); - - if (gameVersion == GameVersion.LittleBigPlanetVita || gameVersion == GameVersion.LittleBigPlanetPSP || gameVersion == GameVersion.Unknown) - { - query = query.Where(s => s.GameVersion == gameVersion); - } - else - { - query = query.Where(s => s.GameVersion <= gameVersion); - } - - if (!includeSublevels) query = query.Where(s => !s.SubLevel); - - return query; - } - - public static IQueryable ByGameVersion(this IQueryable queryable, GameVersion gameVersion, bool includeSublevels = false) - { - IQueryable query = queryable; - - if (gameVersion == GameVersion.LittleBigPlanetVita || gameVersion == GameVersion.LittleBigPlanetPSP || gameVersion == GameVersion.Unknown) - { - query = query.Where(r => r.Slot.GameVersion == gameVersion); - } - else - { - query = query.Where(r => r.Slot.GameVersion <= gameVersion); - } - - if (!includeSublevels) query = query.Where(r => !r.Slot.SubLevel); - - return query; - } - public static async Task Has(this IQueryable queryable, Expression> predicate) => await queryable.FirstOrDefaultAsync(predicate) != null; diff --git a/ProjectLighthouse/Extensions/PredicateExtensions.cs b/ProjectLighthouse/Extensions/PredicateExtensions.cs new file mode 100644 index 00000000..b2e39470 --- /dev/null +++ b/ProjectLighthouse/Extensions/PredicateExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class PredicateExtensions +{ + public static Expression> True() + { + return f => true; + } + + public static Expression> False() + { + return f => false; + } + + public static Expression> Or(this Expression> expr1, Expression> expr2) + { + InvocationExpression invokedExpr = Expression.Invoke(expr2, expr1.Parameters); + return Expression.Lambda>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); + } + + public static Expression> And + (this Expression> expr1, Expression> expr2) + { + InvocationExpression invokedExpr = Expression.Invoke(expr2, expr1.Parameters); + return Expression.Lambda>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/QueryExtensions.cs b/ProjectLighthouse/Extensions/QueryExtensions.cs index 02d34700..6fe51556 100644 --- a/ProjectLighthouse/Extensions/QueryExtensions.cs +++ b/ProjectLighthouse/Extensions/QueryExtensions.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Logging; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Extensions; @@ -8,4 +13,19 @@ public static class QueryExtensions { public static List ToSerializableList(this IEnumerable enumerable, Func selector) => enumerable.Select(selector).ToList(); + + public static IQueryable ApplyPagination(this IQueryable queryable, PaginationData pagination) + { + if (pagination.MaxElements <= 0) + { + Logger.Warn($"ApplyPagination() called with MaxElements of {pagination.MaxElements}\n{queryable.ToQueryString()}", LogArea.Database); + pagination.MaxElements = pagination.PageSize; + } + queryable = queryable.Skip(Math.Max(0, pagination.PageStart - 1)); + return queryable.Take(Math.Min(pagination.PageSize, Math.Min(1000, pagination.MaxElements))); + } + + public static IOrderedQueryable ApplyOrdering + (this IQueryable queryable, ISortBuilder sortBuilder) => + sortBuilder.Build(queryable); } \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/RequestExtensions.cs b/ProjectLighthouse/Extensions/RequestExtensions.cs index d85c4421..0354e56e 100644 --- a/ProjectLighthouse/Extensions/RequestExtensions.cs +++ b/ProjectLighthouse/Extensions/RequestExtensions.cs @@ -1,5 +1,7 @@ #nullable enable using System.Text.RegularExpressions; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Types.Filter; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -7,6 +9,25 @@ namespace LBPUnion.ProjectLighthouse.Extensions; public static partial class RequestExtensions { + + public static PaginationData GetPaginationData(this HttpRequest request) + { + int start = int.TryParse(request.Query["pageStart"], out int pageStart) ? pageStart : 0; + int size = int.TryParse(request.Query["pageSize"], out int pageSize) + ? pageSize + : ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots; + + if (start < 0) start = 0; + if (size <= 0) size = 10; + + PaginationData paginationData = new() + { + PageStart = start, + PageSize = size, + }; + return paginationData; + } + #region Mobile Checking // yoinked and adapted from https://stackoverflow.com/a/68641796 diff --git a/ProjectLighthouse/Filter/Filters/AdventureFilter.cs b/ProjectLighthouse/Filter/Filters/AdventureFilter.cs new file mode 100644 index 00000000..a233074e --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/AdventureFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class AdventureFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.IsAdventurePlanet); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs b/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs new file mode 100644 index 00000000..e203cf67 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/AuthorLabelFilter.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class AuthorLabelFilter : ISlotFilter +{ + private readonly string[] labels; + + public AuthorLabelFilter(params string[] labels) + { + this.labels = labels; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.True(); + predicate = this.labels.Aggregate(predicate, + (current, label) => current.And(s => s.AuthorLabels.Contains(label))); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/CreatorFilter.cs b/ProjectLighthouse/Filter/Filters/CreatorFilter.cs new file mode 100644 index 00000000..ba7f4da8 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/CreatorFilter.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class CreatorFilter : ISlotFilter +{ + private readonly int creatorId; + + public CreatorFilter(int creatorId) + { + this.creatorId = creatorId; + } + + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.CreatorId == this.creatorId); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs b/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs new file mode 100644 index 00000000..5e98187e --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/CrossControlFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class CrossControlFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.CrossControllerRequired); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs b/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs new file mode 100644 index 00000000..c6a1dae0 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/ExcludeAdventureFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class ExcludeAdventureFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => !s.IsAdventurePlanet); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs b/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs new file mode 100644 index 00000000..f1a464bb --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/ExcludeLBP1OnlyFilter.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Users; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class ExcludeLBP1OnlyFilter : ISlotFilter +{ + private readonly int userId; + private readonly GameVersion targetGameVersion; + + public ExcludeLBP1OnlyFilter(int userId, GameVersion targetGameVersion) + { + this.userId = userId; + this.targetGameVersion = targetGameVersion; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.True(); + predicate = predicate.And(s => !s.Lbp1Only || s.CreatorId == this.userId || this.targetGameVersion == GameVersion.LittleBigPlanet1); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs b/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs new file mode 100644 index 00000000..85d9ccde --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/ExcludeMovePackFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class ExcludeMovePackFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => !s.MoveRequired); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs b/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs new file mode 100644 index 00000000..07e4d865 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/FirstUploadedFilter.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class FirstUploadedFilter : ISlotFilter +{ + private readonly long start; + private readonly long end; + + public FirstUploadedFilter(long start, long end = long.MaxValue) + { + this.start = start; + this.end = end; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.True(); + predicate = predicate.And(s => s.FirstUploaded > this.start); + + // Exclude to optimize query + if (this.end != long.MaxValue) predicate = predicate.And(s => s.FirstUploaded < this.end); + + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs b/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs new file mode 100644 index 00000000..298abefa --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/GameVersionFilter.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Users; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class GameVersionFilter : ISlotFilter +{ + private readonly GameVersion targetVersion; + private readonly bool matchExactly; + + public GameVersionFilter(GameVersion targetVersion, bool matchExactly = false) + { + this.targetVersion = targetVersion; + this.matchExactly = matchExactly; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.True(); + predicate = this.matchExactly || this.targetVersion is GameVersion.LittleBigPlanetVita or GameVersion.LittleBigPlanetPSP or GameVersion.Unknown + ? predicate.And(s => s.GameVersion == this.targetVersion) + : predicate.And(s => s.GameVersion <= this.targetVersion); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs b/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs new file mode 100644 index 00000000..813b2e75 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/GameVersionListFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Users; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class GameVersionListFilter : ISlotFilter +{ + private readonly GameVersion[] versions; + + public GameVersionListFilter(params GameVersion[] versions) + { + this.versions = versions; + } + + public Expression> GetPredicate() => + this.versions.Aggregate(PredicateExtensions.False(), + (current, version) => current.Or(s => s.GameVersion == version)); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs b/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs new file mode 100644 index 00000000..f722c50f --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/HiddenSlotFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class HiddenSlotFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => !s.Hidden); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/MovePackFilter.cs b/ProjectLighthouse/Filter/Filters/MovePackFilter.cs new file mode 100644 index 00000000..65658a8d --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/MovePackFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class MovePackFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.MoveRequired); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs b/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs new file mode 100644 index 00000000..71a64733 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/PlayerCountFilter.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class PlayerCountFilter : ISlotFilter +{ + private readonly int minPlayers; + private readonly int maxPlayers; + + public PlayerCountFilter(int minPlayers = 1, int maxPlayers = 4) + { + this.minPlayers = minPlayers; + this.maxPlayers = maxPlayers; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.True(); + predicate = predicate.And(s => s.MinimumPlayers >= this.minPlayers); + predicate = predicate.And(s => s.MaximumPlayers <= this.maxPlayers); + + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs b/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs new file mode 100644 index 00000000..5e8ce64c --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/ResultTypeFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class ResultTypeFilter : ISlotFilter +{ + private readonly string[] results; + + public ResultTypeFilter(params string[] results) + { + this.results = results; + } + + public Expression> GetPredicate() => + this.results.Contains("slot") + ? PredicateExtensions.True() + : PredicateExtensions.False(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs b/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs new file mode 100644 index 00000000..17412712 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/SlotIdFilter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class SlotIdFilter : ISlotFilter +{ + private readonly List slotIds; + + public SlotIdFilter(List slotIds) + { + this.slotIds = slotIds; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + predicate = this.slotIds.Aggregate(predicate, (current, slotId) => current.Or(s => s.SlotId == slotId)); + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs b/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs new file mode 100644 index 00000000..34cfd5b0 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/SlotTypeFilter.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; +using LBPUnion.ProjectLighthouse.Types.Levels; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class SlotTypeFilter : ISlotFilter +{ + private readonly SlotType slotType; + + public SlotTypeFilter(SlotType slotType) + { + this.slotType = slotType; + } + + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.Type == this.slotType); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs b/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs new file mode 100644 index 00000000..4ca2a0f8 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/SubLevelFilter.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class SubLevelFilter : ISlotFilter +{ + private readonly int userId; + + public SubLevelFilter(int userId) + { + this.userId = userId; + } + + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => !s.SubLevel || s.CreatorId == this.userId); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs b/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs new file mode 100644 index 00000000..01ec2c4c --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/TeamPickFilter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class TeamPickFilter : ISlotFilter +{ + public Expression> GetPredicate() => + PredicateExtensions.True().And(s => s.TeamPick); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Filters/TextFilter.cs b/ProjectLighthouse/Filter/Filters/TextFilter.cs new file mode 100644 index 00000000..7566ca44 --- /dev/null +++ b/ProjectLighthouse/Filter/Filters/TextFilter.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter.Filters; + +public class TextFilter : ISlotFilter +{ + private readonly string filter; + + public TextFilter(string filter) + { + this.filter = filter; + } + + public Expression> GetPredicate() + { + Expression> predicate = PredicateExtensions.False(); + string[] keywords = this.filter.Split(" ", StringSplitOptions.RemoveEmptyEntries); + foreach (string keyword in keywords) + { + predicate = predicate.Or(s => + s.Name.Contains(keyword) || + s.Description.ToLower().Contains(keyword) || + s.SlotId.ToString().Equals(keyword)); + predicate = predicate.Or(s => s.Creator != null && s.Creator.Username.Contains(keyword)); + } + return predicate; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/SlotQueryBuilder.cs b/ProjectLighthouse/Filter/SlotQueryBuilder.cs new file mode 100644 index 00000000..0fce7c4e --- /dev/null +++ b/ProjectLighthouse/Filter/SlotQueryBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Filter; + +public class SlotQueryBuilder : IQueryBuilder +{ + private readonly List filters; + + public SlotQueryBuilder() + { + this.filters = new List(); + } + + public Expression> Build() + { + Expression> predicate = PredicateExtensions.True(); + predicate = this.filters.Aggregate(predicate, (current, filter) => current.And(filter.GetPredicate())); + return predicate; + } + + public SlotQueryBuilder RemoveFilter(Type type) + { + this.filters.RemoveAll(f => f.GetType() == type); + return this; + } + + #nullable enable + public IEnumerable GetFilters(Type type) => this.filters.Where(f => f.GetType() == type).ToList(); + #nullable disable + + public SlotQueryBuilder AddFilter(int index, ISlotFilter filter) + { + this.filters.Insert(index, filter); + return this; + } + + public SlotQueryBuilder Clone() + { + SlotQueryBuilder clone = new(); + clone.filters.AddRange(this.filters); + return clone; + } + + public SlotQueryBuilder AddFilter(ISlotFilter filter) + { + this.filters.Add(filter); + return this; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/SlotSortBuilder.cs b/ProjectLighthouse/Filter/SlotSortBuilder.cs new file mode 100644 index 00000000..fd3ce936 --- /dev/null +++ b/ProjectLighthouse/Filter/SlotSortBuilder.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter; + +public class SlotSortBuilder : ISortBuilder +{ + private readonly List> sorts; + private bool sortDescending; + + public SlotSortBuilder() + { + this.sorts = new List>(); + this.sortDescending = true; + } + + public SlotSortBuilder AddSort(ISort sort) + { + this.sorts.Add(sort); + return this; + } + + public SlotSortBuilder SortDescending(bool descending) + { + this.sortDescending = descending; + return this; + } + + public IOrderedQueryable Build(IQueryable queryable) + { + IOrderedQueryable orderedQueryable = (IOrderedQueryable)queryable; + // Probably not the best way to do this but to convert from IQueryable to IOrderedQueryable you have to + // OrderBy some field before you can call ThenBy. One way to do this is call OrderBy(s => 0) but this + // generates some extra SQL so I've settled on this + bool usedFirstOrder = false; + foreach (ISort sort in this.sorts) + { + if (this.sortDescending) + { + orderedQueryable = !usedFirstOrder + ? orderedQueryable.OrderByDescending(sort.GetExpression()) + : orderedQueryable.ThenByDescending(sort.GetExpression()); + } + else + { + orderedQueryable = !usedFirstOrder + ? orderedQueryable.OrderBy(sort.GetExpression()) + : orderedQueryable.ThenBy(sort.GetExpression()); + } + + usedFirstOrder = true; + } + return orderedQueryable; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/FirstUploadedSort.cs b/ProjectLighthouse/Filter/Sorts/FirstUploadedSort.cs new file mode 100644 index 00000000..58c5b404 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/FirstUploadedSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class FirstUploadedSort : ISlotSort +{ + public Expression> GetExpression() => s => s.FirstUploaded; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/LastUpdatedSort.cs b/ProjectLighthouse/Filter/Sorts/LastUpdatedSort.cs new file mode 100644 index 00000000..9692c486 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/LastUpdatedSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class LastUpdatedSort : ISlotSort +{ + public Expression> GetExpression() => s => s.LastUpdated; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/Metadata/HeartsSort.cs b/ProjectLighthouse/Filter/Sorts/Metadata/HeartsSort.cs new file mode 100644 index 00000000..92f378c4 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/Metadata/HeartsSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Misc; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; + +public class HeartsSort : ISort +{ + public Expression> GetExpression() => s => s.Hearts; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/Metadata/RatingLBP1Sort.cs b/ProjectLighthouse/Filter/Sorts/Metadata/RatingLBP1Sort.cs new file mode 100644 index 00000000..f9c73a94 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/Metadata/RatingLBP1Sort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Misc; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; + +public class RatingLBP1Sort : ISort +{ + public Expression> GetExpression() => s => s.RatingLbp1; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/Metadata/ThumbsUpSort.cs b/ProjectLighthouse/Filter/Sorts/Metadata/ThumbsUpSort.cs new file mode 100644 index 00000000..5f3e132f --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/Metadata/ThumbsUpSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Misc; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata; + +public class ThumbsUpSort : ISort +{ + public Expression> GetExpression() => s => s.ThumbsUp; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/PlaysForGameSort.cs b/ProjectLighthouse/Filter/Sorts/PlaysForGameSort.cs new file mode 100644 index 00000000..2c3af3a0 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/PlaysForGameSort.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class PlaysForGameSort : ISlotSort +{ + private readonly GameVersion targetVersion; + + public PlaysForGameSort(GameVersion targetVersion) + { + this.targetVersion = targetVersion; + } + + private string GetColName() => + this.targetVersion switch + { + GameVersion.LittleBigPlanet1 => "LBP1", + GameVersion.LittleBigPlanet2 => "LBP2", + GameVersion.LittleBigPlanet3 => "LBP3", + GameVersion.LittleBigPlanetVita => "LBP2", + _ => "", + }; + + public Expression> GetExpression() => s => EF.Property(s, $"Plays{this.GetColName()}"); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/RandomFirstUploadedSort.cs b/ProjectLighthouse/Filter/Sorts/RandomFirstUploadedSort.cs new file mode 100644 index 00000000..9b0d084b --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/RandomFirstUploadedSort.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class RandomFirstUploadedSort : ISlotSort +{ + private const double biasFactor = .8f; + + public Expression> GetExpression() => + s => EF.Functions.Random() * (s.FirstUploaded * biasFactor); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/RandomSort.cs b/ProjectLighthouse/Filter/Sorts/RandomSort.cs new file mode 100644 index 00000000..e6f16fcd --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/RandomSort.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class RandomSort : ISlotSort +{ + public Expression> GetExpression() => _ => EF.Functions.Random(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/SlotIdSort.cs b/ProjectLighthouse/Filter/Sorts/SlotIdSort.cs new file mode 100644 index 00000000..d7967425 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/SlotIdSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class SlotIdSort : ISlotSort +{ + public Expression> GetExpression() => s => s.SlotId; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/TotalPlaysSort.cs b/ProjectLighthouse/Filter/Sorts/TotalPlaysSort.cs new file mode 100644 index 00000000..1db5e414 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/TotalPlaysSort.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class TotalPlaysSort : ISlotSort +{ + public Expression> GetExpression() => s => s.PlaysLBP1 + s.PlaysLBP2 + s.PlaysLBP3; +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/UniquePlaysForGameSort.cs b/ProjectLighthouse/Filter/Sorts/UniquePlaysForGameSort.cs new file mode 100644 index 00000000..b172d4ff --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/UniquePlaysForGameSort.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class UniquePlaysForGameSort : ISlotSort +{ + private readonly GameVersion targetVersion; + + public UniquePlaysForGameSort(GameVersion targetVersion) + { + this.targetVersion = targetVersion; + } + + private string GetColName() => + this.targetVersion switch + { + GameVersion.LittleBigPlanet1 => "LBP1", + GameVersion.LittleBigPlanet2 => "LBP2", + GameVersion.LittleBigPlanet3 => "LBP3", + GameVersion.LittleBigPlanetVita => "LBP2", + _ => "", + }; + + public Expression> GetExpression() => s => EF.Property(s, $"Plays{this.GetColName()}Unique"); +} \ No newline at end of file diff --git a/ProjectLighthouse/Filter/Sorts/UniquePlaysTotalSort.cs b/ProjectLighthouse/Filter/Sorts/UniquePlaysTotalSort.cs new file mode 100644 index 00000000..886d9497 --- /dev/null +++ b/ProjectLighthouse/Filter/Sorts/UniquePlaysTotalSort.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq.Expressions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +namespace LBPUnion.ProjectLighthouse.Filter.Sorts; + +public class UniquePlaysTotalSort : ISlotSort +{ + public Expression> GetExpression() => + s => s.PlaysLBP1Unique + s.PlaysLBP2Unique + s.PlaysLBP3Unique; +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/SlotHelper.cs b/ProjectLighthouse/Helpers/SlotHelper.cs index 0f0abf07..b46a3b5a 100644 --- a/ProjectLighthouse/Helpers/SlotHelper.cs +++ b/ProjectLighthouse/Helpers/SlotHelper.cs @@ -4,9 +4,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; @@ -14,7 +16,6 @@ namespace LBPUnion.ProjectLighthouse.Helpers; public static class SlotHelper { - public static SlotType ParseType(string? slotType) { if (slotType == null) return SlotType.Unknown; @@ -41,7 +42,8 @@ public static class SlotHelper }; } - private static readonly SemaphoreSlim semaphore = new(1, 1); + private static readonly SemaphoreSlim slotSemaphore = new(1, 1); + private static readonly SemaphoreSlim userSemaphore = new(1, 1); public static async Task GetPlaceholderUserId(DatabaseContext database) { @@ -50,7 +52,13 @@ public static class SlotHelper .FirstOrDefaultAsync(); if (devCreatorId != 0) return devCreatorId; - await semaphore.WaitAsync(TimeSpan.FromSeconds(5)); + bool acquired = await userSemaphore.WaitAsync(TimeSpan.FromSeconds(5)); + if (!acquired) + { + Logger.Warn($"Failed to acquire lock for placeholder user, semaphoreCount={userSemaphore.CurrentCount}", + LogArea.Synchronization); + return 0; + } try { UserEntity devCreator = new() @@ -66,7 +74,7 @@ public static class SlotHelper } finally { - semaphore.Release(); + if (acquired) userSemaphore.Release(); } } @@ -75,7 +83,14 @@ public static class SlotHelper int slotId = await database.Slots.Where(s => s.Type == slotType && s.InternalSlotId == guid).Select(s => s.SlotId).FirstOrDefaultAsync(); if (slotId != 0) return slotId; - await semaphore.WaitAsync(TimeSpan.FromSeconds(5)); + bool acquired = await slotSemaphore.WaitAsync(TimeSpan.FromSeconds(5)); + if (!acquired) + { + Logger.Warn( + $"Failed to acquire lock for placeholder slot, guid={guid}, slotType={slotType}, semaphoreCount={slotSemaphore.CurrentCount}", + LogArea.Synchronization); + return 0; + } try { // if two requests come in at the same time for the same story level which hasn't been generated @@ -104,7 +119,7 @@ public static class SlotHelper } finally { - semaphore.Release(); + if(acquired) slotSemaphore.Release(); } } } \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/StatisticsHelper.cs b/ProjectLighthouse/Helpers/StatisticsHelper.cs index 88c6063e..92e20379 100644 --- a/ProjectLighthouse/Helpers/StatisticsHelper.cs +++ b/ProjectLighthouse/Helpers/StatisticsHelper.cs @@ -1,9 +1,7 @@ using System.Linq; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Extensions; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms; +using LBPUnion.ProjectLighthouse.Filter; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.EntityFrameworkCore; @@ -17,18 +15,12 @@ public static class StatisticsHelper public static async Task RecentMatchesForGame(DatabaseContext database, GameVersion gameVersion) => await database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300 && l.GameVersion == gameVersion).CountAsync(); - public static async Task SlotCount(DatabaseContext database) => await database.Slots.Where(s => s.Type == SlotType.User).CountAsync(); - - public static async Task SlotCountForGame(DatabaseContext database, GameVersion gameVersion, bool includeSublevels = false) => await database.Slots.ByGameVersion(gameVersion, includeSublevels).CountAsync(); + public static async Task SlotCount(DatabaseContext database, SlotQueryBuilder queryBuilder) => await database.Slots.Where(queryBuilder.Build()).CountAsync(); public static async Task UserCount(DatabaseContext database) => await database.Users.CountAsync(u => u.PermissionLevel != PermissionLevel.Banned); public static int RoomCountForPlatform(Platform targetPlatform) => RoomHelper.Rooms.Count(r => r.IsLookingForPlayers && r.RoomPlatform == targetPlatform); - public static async Task TeamPickCount(DatabaseContext database) => await database.Slots.CountAsync(s => s.TeamPick); - - public static async Task TeamPickCountForGame(DatabaseContext database, GameVersion gameVersion, bool? crosscontrol = null) => await database.Slots.ByGameVersion(gameVersion).CountAsync(s => s.TeamPick && (crosscontrol == null || s.CrossControllerRequired == crosscontrol)); - public static async Task PhotoCount(DatabaseContext database) => await database.Photos.CountAsync(); #region Moderator/Admin specific diff --git a/ProjectLighthouse/Mail/MailQueueService.cs b/ProjectLighthouse/Mail/MailQueueService.cs index ea5f63e8..f0a62828 100644 --- a/ProjectLighthouse/Mail/MailQueueService.cs +++ b/ProjectLighthouse/Mail/MailQueueService.cs @@ -34,7 +34,7 @@ public class MailQueueService : IMailService, IDisposable this.emailThread = Task.Factory.StartNew(this.EmailQueue); } - private async void EmailQueue() + private async Task EmailQueue() { while (!this.stopSignal) { diff --git a/ProjectLighthouse/Types/Filter/IFilter.cs b/ProjectLighthouse/Types/Filter/IFilter.cs new file mode 100644 index 00000000..7dd16b21 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/IFilter.cs @@ -0,0 +1,9 @@ +using System; +using System.Linq.Expressions; + +namespace LBPUnion.ProjectLighthouse.Types.Filter; + +public interface IFilter +{ + public Expression> GetPredicate(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/IQueryBuilder.cs b/ProjectLighthouse/Types/Filter/IQueryBuilder.cs new file mode 100644 index 00000000..78b3e5a5 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/IQueryBuilder.cs @@ -0,0 +1,9 @@ +using System; +using System.Linq.Expressions; + +namespace LBPUnion.ProjectLighthouse.Types.Filter; + +public interface IQueryBuilder +{ + public Expression> Build(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/ISlotFilter.cs b/ProjectLighthouse/Types/Filter/ISlotFilter.cs new file mode 100644 index 00000000..2dff1138 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/ISlotFilter.cs @@ -0,0 +1,6 @@ +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Filter; + +public interface ISlotFilter : IFilter +{ } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/PaginationData.cs b/ProjectLighthouse/Types/Filter/PaginationData.cs new file mode 100644 index 00000000..d1f9ee90 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/PaginationData.cs @@ -0,0 +1,16 @@ +using System; + +namespace LBPUnion.ProjectLighthouse.Types.Filter; + +public struct PaginationData +{ + public PaginationData() + { } + + public int PageStart { get; init; } = 0; + public int PageSize { get; init; } = 0; + public int TotalElements { get; set; } = 0; + public int MaxElements { get; set; } = 30; + + public int HintStart => this.PageStart + Math.Min(this.PageSize, this.MaxElements); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/Sorts/ISlotSort.cs b/ProjectLighthouse/Types/Filter/Sorts/ISlotSort.cs new file mode 100644 index 00000000..499c0d84 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/Sorts/ISlotSort.cs @@ -0,0 +1,6 @@ +using LBPUnion.ProjectLighthouse.Types.Entities.Level; + +namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +public interface ISlotSort : ISort +{ } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/Sorts/ISort.cs b/ProjectLighthouse/Types/Filter/Sorts/ISort.cs new file mode 100644 index 00000000..4cd347a1 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/Sorts/ISort.cs @@ -0,0 +1,9 @@ +using System; +using System.Linq.Expressions; + +namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +public interface ISort +{ + public Expression> GetExpression(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Filter/Sorts/ISortBuilder.cs b/ProjectLighthouse/Types/Filter/Sorts/ISortBuilder.cs new file mode 100644 index 00000000..15201e91 --- /dev/null +++ b/ProjectLighthouse/Types/Filter/Sorts/ISortBuilder.cs @@ -0,0 +1,8 @@ +using System.Linq; + +namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts; + +public interface ISortBuilder +{ + public IOrderedQueryable Build(IQueryable queryable); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Levels/Category.cs b/ProjectLighthouse/Types/Levels/Category.cs index 33579472..c18a4a00 100644 --- a/ProjectLighthouse/Types/Levels/Category.cs +++ b/ProjectLighthouse/Types/Levels/Category.cs @@ -1,49 +1,31 @@ #nullable enable using System.Collections.Generic; -using System.Xml.Serialization; +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Filter; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Serialization; -using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Types.Levels; -[XmlType("category")] -[XmlRoot("category")] public abstract class Category { - [XmlElement("name")] public abstract string Name { get; set; } - [XmlElement("description")] public abstract string Description { get; set; } - [XmlElement("icon")] public abstract string IconHash { get; set; } - [XmlIgnore] public abstract string Endpoint { get; set; } - [XmlElement("url")] - public string IngameEndpoint { - get => $"/searches/{this.Endpoint}"; - set => this.Endpoint = value.Replace("/searches/", ""); - } + public string[] Sorts { get; } = { "relevance", "likes", "plays", "hearts", "date", }; - public abstract SlotEntity? GetPreviewSlot(DatabaseContext database); + public abstract string[] Types { get; } - public abstract IEnumerable GetSlots(DatabaseContext database, int pageStart, int pageSize); + public abstract string Tag { get; } - public abstract int GetTotalSlots(DatabaseContext database); + public string IngameEndpoint => $"/searches/{this.Endpoint}"; - public GameCategory Serialize(DatabaseContext database) - { - List slots = new(); - SlotEntity? previewSlot = this.GetPreviewSlot(database); - if (previewSlot != null) - slots.Add(SlotBase.CreateFromEntity(previewSlot, GameVersion.LittleBigPlanet3, -1)); - - int totalSlots = this.GetTotalSlots(database); - return GameCategory.CreateFromEntity(this, new GenericSlotResponse(slots, totalSlots, 2)); - } + public virtual Task Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1) => + Task.FromResult(GameCategory.CreateFromEntity(this, new GenericSerializableList(new List(), 0, 0))); } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Logging/LogArea.cs b/ProjectLighthouse/Types/Logging/LogArea.cs index 3b02edbe..10146e22 100644 --- a/ProjectLighthouse/Types/Logging/LogArea.cs +++ b/ProjectLighthouse/Types/Logging/LogArea.cs @@ -27,4 +27,5 @@ public enum LogArea Deserialization, Email, Serialization, + Synchronization, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Misc/SlotMetadata.cs b/ProjectLighthouse/Types/Misc/SlotMetadata.cs index 670865c1..4d1adc6c 100644 --- a/ProjectLighthouse/Types/Misc/SlotMetadata.cs +++ b/ProjectLighthouse/Types/Misc/SlotMetadata.cs @@ -8,4 +8,5 @@ public class SlotMetadata public double RatingLbp1 { get; init; } public int ThumbsUp { get; init; } public int Hearts { get; init; } + public bool Played { get; set; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs b/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs index 2edb440f..5a8715f1 100644 --- a/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs +++ b/ProjectLighthouse/Types/Serialization/CategoryListResponse.cs @@ -8,7 +8,7 @@ public class CategoryListResponse : ILbpSerializable { public CategoryListResponse() { } - public CategoryListResponse(List categories, int total, int hint, int hintStart) + public CategoryListResponse(List categories, int total, string hint, int hintStart) { this.Categories = categories; this.Total = total; @@ -20,7 +20,7 @@ public class CategoryListResponse : ILbpSerializable public int Total { get; set; } [XmlAttribute("hint")] - public int Hint { get; set; } + public string Hint { get; set; } = ""; [XmlAttribute("hint_start")] public int HintStart { get; set; } diff --git a/ProjectLighthouse/Types/Serialization/GameCategory.cs b/ProjectLighthouse/Types/Serialization/GameCategory.cs index 3bd0af02..37037069 100644 --- a/ProjectLighthouse/Types/Serialization/GameCategory.cs +++ b/ProjectLighthouse/Types/Serialization/GameCategory.cs @@ -1,4 +1,5 @@ -using System.Xml.Serialization; +using System.ComponentModel; +using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Levels; namespace LBPUnion.ProjectLighthouse.Types.Serialization; @@ -18,19 +19,32 @@ public class GameCategory : ILbpSerializable [XmlElement("icon")] public string Icon { get; set; } - [XmlElement("results")] - public GenericSlotResponse Results { get; set; } + [DefaultValue("")] + [XmlArray("sorts")] + [XmlArrayItem("sort")] + public string[] Sorts { get; set; } - public static GameCategory CreateFromEntity(Category category, GenericSlotResponse results) => + [DefaultValue("")] + [XmlArray("types")] + [XmlArrayItem("type")] + public string[] Types { get; set; } + + [XmlElement("tag")] + public string Tag { get; set; } + + [XmlElement("results")] + public GenericSerializableList Results { get; set; } + + public static GameCategory CreateFromEntity(Category category, GenericSerializableList results) => new() { Name = category.Name, Description = category.Description, Icon = category.IconHash, Url = category.IngameEndpoint, + Sorts = category.Sorts, + Types = category.Types, + Tag = category.Tag, Results = results, }; - - - } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs b/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs index d149dfe3..e1a83115 100644 --- a/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs +++ b/ProjectLighthouse/Types/Serialization/GameDeveloperSlot.cs @@ -33,10 +33,12 @@ public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization public async Task PrepareSerialization(DatabaseContext database) { var stats = await database.Slots.Select(_ => new - { - CommentCount = database.Comments.Count(c => c.TargetId == this.SlotId && c.Type == CommentType.Level), - PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId), - }).FirstAsync(); + { + CommentCount = database.Comments.Count(c => c.TargetId == this.SlotId && c.Type == CommentType.Level), + PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId), + }) + .OrderBy(_ => 1) + .FirstAsync(); ReflectionHelper.CopyAllFields(stats, this); this.PlayerCount = RoomHelper.Rooms .Where(r => r.Slot.SlotType == SlotType.Developer && r.Slot.SlotId == this.InternalSlotId) diff --git a/ProjectLighthouse/Types/Serialization/GameUser.cs b/ProjectLighthouse/Types/Serialization/GameUser.cs index ab062707..07cc2b32 100644 --- a/ProjectLighthouse/Types/Serialization/GameUser.cs +++ b/ProjectLighthouse/Types/Serialization/GameUser.cs @@ -179,7 +179,8 @@ public class GameUser : ILbpSerializable, INeedsPreparationForSerialization HeartedPlaylistCount = database.HeartedPlaylists.Count(h => h.UserId == this.UserId), QueuedLevelCount = database.QueuedLevels.Count(q => q.UserId == this.UserId), }) - .FirstOrDefaultAsync(); + .OrderBy(_ => 1) + .FirstAsync(); this.UserHandle.Username = stats.Username; this.CommentsEnabled = this.CommentsEnabled && ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled; diff --git a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs b/ProjectLighthouse/Types/Serialization/GameUserSlot.cs index 5f45bc2d..6e3d902a 100644 --- a/ProjectLighthouse/Types/Serialization/GameUserSlot.cs +++ b/ProjectLighthouse/Types/Serialization/GameUserSlot.cs @@ -247,6 +247,7 @@ public class GameUserSlot : SlotBase, INeedsPreparationForSerialization HeartCount = database.HeartedLevels.Count(h => h.SlotId == this.SlotId), Username = database.Users.Where(u => u.UserId == this.CreatorId).Select(u => u.Username).First(), }) + .OrderBy(_ => 1) .FirstAsync(); ReflectionHelper.CopyAllFields(stats, this); this.AuthorHandle = new NpHandle(stats.Username, ""); diff --git a/ProjectLighthouse/Types/Serialization/GenericSerializableList.cs b/ProjectLighthouse/Types/Serialization/GenericSerializableList.cs new file mode 100644 index 00000000..7a1fd2b1 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/GenericSerializableList.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization; + +[XmlRoot("results")] +public struct GenericSerializableList : ILbpSerializable +{ + public GenericSerializableList(List items, int total, int hintStart) + { + this.Items = new SerializableList(); + this.Items.AddRange(items); + this.Total = total; + this.HintStart = hintStart; + } + + public GenericSerializableList(List items, PaginationData pageData) + { + this.Items = new SerializableList(); + this.Items.AddRange(items); + this.Total = pageData.TotalElements; + this.HintStart = pageData.HintStart; + } + + [XmlAnyElement] + public SerializableList Items { get; set; } + + [XmlAttribute("total")] + public int Total { get; set; } + + [XmlAttribute("hint_start")] + public int HintStart { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs b/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs index 2b6a7dc4..b00e8575 100644 --- a/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs +++ b/ProjectLighthouse/Types/Serialization/GenericSlotResponse.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Filter; namespace LBPUnion.ProjectLighthouse.Types.Serialization; @@ -16,6 +17,16 @@ public struct GenericSlotResponse : ILbpSerializable, IHasCustomRoot this.HintStart = hintStart; } + public GenericSlotResponse(string rootElement, List slots, PaginationData pageData) + { + this.RootTag = rootElement; + this.Slots = slots; + this.Total = pageData.TotalElements; + this.HintStart = pageData.HintStart; + } + + public GenericSlotResponse(List slots, PaginationData pageData) : this("slots", slots, pageData) { } + public GenericSlotResponse(List slots) : this("slots", slots) { } public GenericSlotResponse(List slots, int total, int hintStart) : this("slots", slots, total, hintStart) { } diff --git a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs b/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs index 1f398644..ef97d42c 100644 --- a/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs +++ b/ProjectLighthouse/Types/Serialization/GenericUserResponse.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Types.Filter; namespace LBPUnion.ProjectLighthouse.Types.Serialization; @@ -16,6 +17,14 @@ public struct GenericUserResponse : ILbpSerializable, IHasCustomRoot where T this.HintStart = hintStart; } + public GenericUserResponse(string rootElement, List users, PaginationData pageData) + { + this.RootTag = rootElement; + this.Users = users; + this.Total = pageData.TotalElements; + this.HintStart = pageData.HintStart; + } + public GenericUserResponse(string rootElement, List users) { this.RootTag = rootElement; @@ -23,7 +32,7 @@ public struct GenericUserResponse : ILbpSerializable, IHasCustomRoot where T } [XmlIgnore] - public string RootTag { get; set; } + private string RootTag { get; } [XmlElement("user")] public List Users { get; set; } diff --git a/ProjectLighthouse/Types/Serialization/SerializableList.cs b/ProjectLighthouse/Types/Serialization/SerializableList.cs new file mode 100644 index 00000000..1c2c0cf4 --- /dev/null +++ b/ProjectLighthouse/Types/Serialization/SerializableList.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Serialization; + +/// +/// Shitty workaround to allow Lists of ILbpSerializable to be serialized. +/// +public class SerializableList : List, IXmlSerializable where T : ILbpSerializable +{ + public XmlSchema GetSchema() => null; + + public void ReadXml(XmlReader reader) + { + foreach (CustomXmlSerializer xmlSerializer in this.Select(serializable => LighthouseSerializer.GetSerializer(serializable.GetType()))) + { + xmlSerializer.Deserialize(reader); + } + } + + public void WriteXml(XmlWriter writer) + { + foreach (T serializable in this) + { + CustomXmlSerializer xmlSerializer = LighthouseSerializer.GetSerializer(serializable.GetType()); + xmlSerializer.Serialize(writer, serializable); + } + } +} \ No newline at end of file