Rewrite gameserver slot filter system (#763)

* Initial implementation of new slot sorting and filtering system

* Initial implementation of filtering for lbp3 community tab

* Add support for organization on lbp3

* Add playlist and user categories

* Implement unit tests for all filters
Refactor more systems to use PaginationData

* Fix PlayerCountFilter test

* Add more unit tests and integration tests for the filter system

* Fix LBP2 move filter and gameFilterType

* Fix sort by likes in LBP3 category

* Add sort for total plays

* Remove extra whitespace and make styling more consistent

* Order hearted and queued levels by primary key ID

* Fix query without order warnings
This commit is contained in:
Josh 2023-05-31 16:33:39 -05:00 committed by GitHub
parent de228cb242
commit 0c1e350fa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 4040 additions and 1183 deletions

View file

@ -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())),
}
);

View file

@ -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<IActionResult> GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, string? slotType, int slotId)
public async Task<IActionResult> 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));

View file

@ -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]

View file

@ -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")]

View file

@ -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]

View file

@ -64,5 +64,4 @@ public class ReportController : ControllerBase
return this.Ok();
}
}

View file

@ -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<IActionResult> SlotPhotos([FromQuery] int pageStart, [FromQuery] int pageSize, string slotType, int id)
public async Task<IActionResult> 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<GamePhoto> 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<IActionResult> UserPhotosBy([FromQuery] string user, [FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> 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<GamePhoto> 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<IActionResult> UserPhotosWith([FromQuery] string user, [FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> 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<GamePhoto> 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));

View file

@ -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<IActionResult> 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<GameCategory> 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<IActionResult> 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<GenericSerializableList> GetUserCategory(UserCategory userCategory, GameTokenEntity token, PaginationData pageData)
{
int totalUsers = await userCategory.GetItems(this.database, token).CountAsync();
pageData.TotalElements = totalUsers;
IQueryable<UserEntity> userQuery = userCategory.GetItems(this.database, token).ApplyPagination(pageData);
List<ILbpSerializable> users =
(await userQuery.ToListAsync()).ToSerializableList<UserEntity, ILbpSerializable>(GameUser
.CreateFromEntity);
return new GenericSerializableList(users, pageData);
}
private async Task<GenericSerializableList> GetPlaylistCategory(PlaylistCategory playlistCategory, GameTokenEntity token, PaginationData pageData)
{
int totalPlaylists = await playlistCategory.GetItems(this.database, token).CountAsync();
pageData.TotalElements = totalPlaylists;
IQueryable<PlaylistEntity> playlistQuery = playlistCategory.GetItems(this.database, token).ApplyPagination(pageData);
List<ILbpSerializable> playlists =
(await playlistQuery.ToListAsync()).ToSerializableList<PlaylistEntity, ILbpSerializable>(GamePlaylist
.CreateFromEntity);
return new GenericSerializableList(playlists, pageData);
}
private async Task<GenericSerializableList> GetSlotCategory(SlotCategory slotCategory, GameTokenEntity token, SlotQueryBuilder queryBuilder, PaginationData pageData)
{
int totalSlots = await slotCategory.GetItems(this.database, token, queryBuilder).CountAsync();
pageData.TotalElements = totalSlots;
IQueryable<SlotEntity> 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<SlotEntity>()
.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<SlotEntity>().AddSort(new FirstUploadedSort())),
"plays" => slotQuery.ApplyOrdering(
new SlotSortBuilder<SlotEntity>().AddSort(new UniquePlaysTotalSort()).AddSort(new TotalPlaysSort())),
_ => slotQuery,
};
}
List<ILbpSerializable> slots =
(await slotQuery.ToListAsync()).ToSerializableList<SlotEntity, ILbpSerializable>(s =>
SlotBase.CreateFromEntity(s, token));
return new GenericSerializableList(slots, pageData);
}
}

View file

@ -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<IActionResult> GenresAndSearches()
{
GameTokenEntity token = this.GetToken();
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();
List<GameCategory> 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<IActionResult> 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<SlotEntity> 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<SlotEntity> filterSlots(List<SlotEntity> 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;
}
}

View file

@ -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<IActionResult> 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<IActionResult> GetQueuedLevels(string username)
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
List<SlotBase> 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<SlotEntity> baseQuery = this.database.QueuedLevels.Where(h => h.UserId == targetUserId)
.OrderByDescending(q => q.QueuedLevelId)
.Include(q => q.Slot)
.Select(q => q.Slot);
List<SlotBase> queuedLevels = await baseQuery.GetSlots(token,
this.FilterFromRequest(token),
pageData,
new SlotSortBuilder<SlotEntity>());
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<IActionResult> 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<IActionResult> 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<SlotBase> 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);
IQueryable<SlotEntity> baseQuery = this.database.HeartedLevels.Where(h => h.UserId == targetUserId)
.OrderByDescending(h => h.HeartedLevelId)
.Include(h => h.Slot)
.Select(h => h.Slot);
int total = await this.database.HeartedLevels.CountAsync(q => q.UserId == targetUser.UserId);
int start = pageStart + Math.Min(pageSize, 30);
List<SlotBase> heartedLevels = await baseQuery.GetSlots(token,
this.FilterFromRequest(token),
pageData,
new SlotSortBuilder<SlotEntity>());
return this.Ok(new GenericSlotResponse("favouriteSlots", heartedLevels, total, start));
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<IActionResult> 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<IActionResult> GetFavouritePlaylists(string username, [FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> 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<GamePlaylist> 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<GamePlaylist>("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<IActionResult> GetFavouriteUsers(string username, [FromQuery] int pageSize, [FromQuery] int pageStart)
public async Task<IActionResult> 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<GameUser> 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<GameUser>("favouriteUsers", heartedProfiles, total, pageStart + Math.Min(pageSize, 30)));
return this.Ok(new GenericUserResponse<GameUser>("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<SlotEntity> 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<QueuedLevelEntity> 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<HeartedLevelEntity> 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
}

View file

@ -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<IActionResult> Rate(int slotId, [FromQuery] int rating)
public async Task<IActionResult> 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<IActionResult> DPadRate(int slotId, [FromQuery] int rating)
public async Task<IActionResult> 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<IActionResult> ReviewsFor(int slotId, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10)
public async Task<IActionResult> 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<GameReview> reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true)
List<GameReview> 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<IActionResult> ReviewsBy(string username, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10)
public async Task<IActionResult> 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<GameReview> reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true)
List<GameReview> 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<IActionResult> RateReview(int slotId, string username, [FromQuery] int rating = 0)
public async Task<IActionResult> RateReview(int slotId, string username, int rating = 0)
{
GameTokenEntity token = this.GetToken();

View file

@ -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<IActionResult> 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<IActionResult> SearchSlotsLBP3([FromQuery] string textFilter)
=> this.SearchSlots(textFilter, "results");
[HttpGet("search")]
public async Task<IActionResult> 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<IActionResult> 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<SlotEntity> 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<SlotBase> slots = await this.database.Slots.Include(s => s.Creator)
.GetSlots(token, queryBuilder, pageData, new SlotSortBuilder<SlotEntity>());
List<SlotEntity> 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<SlotEntity> filterSlots(List<SlotEntity> 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;
}
}

View file

@ -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<IActionResult> SlotsBy([FromQuery(Name = "u")] string username, [FromQuery] int pageStart, [FromQuery] int pageSize, [FromQuery] bool crosscontrol = false)
public async Task<IActionResult> 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<SlotBase> 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));
pageData.TotalElements = await this.database.Slots.CountAsync(s => s.CreatorId == targetUserId);
int start = pageStart + Math.Min(pageSize, usedSlots);
int total = await this.database.Slots.CountAsync(s => s.CreatorId == targetUserId && s.CrossControllerRequired == crosscontrol);
SlotQueryBuilder queryBuilder = this.FilterFromRequest(token).AddFilter(new CreatorFilter(targetUserId));
return this.Ok(new GenericSlotResponse("slots", slots, total, start));
SlotSortBuilder<SlotEntity> sortBuilder = new SlotSortBuilder<SlotEntity>().AddSort(new FirstUploadedSort());
List<SlotBase> 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<SlotBase> 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<IActionResult> SDev(int id)
public async Task<IActionResult> DeveloperSlot(int id)
{
GameTokenEntity token = this.GetToken();
@ -113,13 +116,12 @@ public class SlotsController : ControllerBase
}
[HttpGet("s/user/{id:int}")]
public async Task<IActionResult> SUser(int id)
public async Task<IActionResult> 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<IActionResult> Lbp1CoolSlots([FromQuery] int page)
{
const int pageSize = 30;
return await this.CoolSlots((page - 1) * pageSize, pageSize);
}
public async Task<IActionResult> Lbp1CoolSlots() => await this.CoolSlots();
[HttpGet("slots/lbp2cool")]
public async Task<IActionResult> 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<IActionResult> CoolSlots() => await this.ThumbsSlots();
[HttpGet("slots")]
public async Task<IActionResult> NewestSlots([FromQuery] int pageStart, [FromQuery] int pageSize, [FromQuery] bool crosscontrol = false)
public async Task<IActionResult> 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<SlotBase> 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<SlotEntity> sortBuilder = new();
sortBuilder.AddSort(new FirstUploadedSort());
sortBuilder.AddSort(new SlotIdSort());
List<SlotBase> 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<IActionResult> SimilarSlots([FromRoute] string slotType, [FromRoute] int slotId, [FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> 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<SlotBase> 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<SlotEntity> sortBuilder = new();
sortBuilder.AddSort(new PlaysForGameSort(GameVersion.LittleBigPlanet1));
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/highestRated")]
public async Task<IActionResult> HighestRatedSlots([FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> 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<SlotBase> 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<SlotMetadata> sortBuilder = new();
sortBuilder.AddSort(new RatingLBP1Sort());
return this.Ok(new GenericSlotResponse(slots, total, start));
Expression<Func<SlotEntity, SlotMetadata>> selectorFunc = s => new SlotMetadata
{
Slot = s,
RatingLbp1 = this.database.RatedLevels.Where(r => r.SlotId == s.SlotId)
.Average(r => (double?)r.RatingLBP1) ?? 3.0,
};
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder, selectorFunc);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/tag")]
public async Task<IActionResult> SimilarSlots([FromQuery] string tag, [FromQuery] int pageStart, [FromQuery] int pageSize)
public async Task<IActionResult> SimilarSlots([FromQuery] string tag)
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
List<int> slotIdsWithTag = await this.database.RatedLevels.Where(r => r.TagLBP1.Length > 0)
.Where(r => r.TagLBP1 == tag)
.Select(s => s.SlotId)
.ToListAsync();
List<SlotBase> 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<SlotEntity> sortBuilder = new();
sortBuilder.AddSort(new PlaysForGameSort(GameVersion.LittleBigPlanet1));
return this.Ok(new GenericSlotResponse(slots, total, start));
List<SlotBase> slots = await this.database.GetSlots(token, this.FilterFromRequest(token), pageData, sortBuilder);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/mmpicks")]
public async Task<IActionResult> 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<IActionResult> TeamPickedSlots()
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
List<SlotBase> 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<SlotEntity> sortBuilder = new();
sortBuilder.AddSort(new LastUpdatedSort());
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/lbp2luckydip")]
public async Task<IActionResult> 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<IActionResult> 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<SlotBase> 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<SlotEntity> sortBuilder = new();
sortBuilder.AddSort(new RandomFirstUploadedSort());
return this.Ok(new GenericSlotResponse(slots, total, start));
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/thumbs")]
public async Task<IActionResult> 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<IActionResult> ThumbsSlots()
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
List<SlotBase> 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<SlotMetadata> sortBuilder = new();
sortBuilder.AddSort(new ThumbsUpSort());
Expression<Func<SlotEntity, SlotMetadata>> selectorFunc = s => new SlotMetadata
{
Slot = s,
ThumbsUp = this.database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1),
};
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, sortBuilder, selectorFunc);
return this.Ok(new GenericSlotResponse(slots, pageData));
}
[HttpGet("slots/mostUniquePlays")]
public async Task<IActionResult> 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<IActionResult> 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<SlotBase> slots = this.filterSlots((await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.Where(s => s.CrossControllerRequired == crosscontrol)
.OrderByDescending(s => EF.Property<int>(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<SlotEntity> 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<SlotBase> 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<IActionResult> 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<IActionResult> MostHeartedSlots()
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
List<SlotBase> 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<SlotMetadata> sortBuilder = new();
sortBuilder.AddSort(new HeartsSort());
Expression<Func<SlotEntity, SlotMetadata>> selectorFunc = s => new SlotMetadata
{
Slot = s,
Hearts = this.database.HeartedLevels.Count(r => r.SlotId == s.SlotId),
};
List<SlotBase> 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<IActionResult> 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<IActionResult> BusiestLevels()
{
GameTokenEntity token = this.GetToken();
if (pageSize <= 0) return this.BadRequest();
PaginationData pageData = this.Request.GetPaginationData();
Dictionary<int, int> playersBySlotId = new();
@ -484,98 +365,15 @@ public class SlotsController : ControllerBase
playersBySlotId.Add(room.Slot.SlotId, playerCount);
}
IEnumerable<int> orderedPlayersBySlotId = playersBySlotId
.Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 30))
.OrderByDescending(kvp => kvp.Value)
.Select(kvp => kvp.Key);
pageData.TotalElements = playersBySlotId.Count;
List<SlotEntity> slots = new();
List<int> orderedPlayersBySlotId = playersBySlotId.OrderByDescending(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
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
SlotQueryBuilder queryBuilder = this.FilterFromRequest(token);
queryBuilder.AddFilter(0, new SlotIdFilter(orderedPlayersBySlotId));
slots.Add(slot);
}
List<SlotBase> slots = await this.database.GetSlots(token, queryBuilder, pageData, new SlotSortBuilder<SlotEntity>());
slots = this.filterSlots(slots, players, labelFilter0, labelFilter1, labelFilter2, move);
int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots);
int total = playersBySlotId.Count;
return this.Ok(new GenericSlotResponse(slots.ToSerializableList(s => SlotBase.CreateFromEntity(s, token)), total, start));
}
private List<SlotEntity> filterSlots(List<SlotEntity> 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<SlotEntity> 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<SlotEntity> 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));
}
}

View file

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> TotalLevelCount() => this.Ok((await StatisticsHelper.SlotCountForGame(this.database, this.GetToken().GameVersion)).ToString());
public async Task<IActionResult> TotalLevelCount()
{
SlotQueryBuilder defaultFilter = this.GetDefaultFilters(this.GetToken());
int totalSlotCount = await StatisticsHelper.SlotCount(this.database, defaultFilter);
return this.Ok(totalSlotCount.ToString());
}
}

View file

@ -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<string> 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;
}
}

View file

@ -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<List<SlotBase>> GetSlots
(
this IQueryable<SlotEntity> queryable,
GameTokenEntity token,
SlotQueryBuilder queryBuilder,
PaginationData pageData,
ISortBuilder<SlotEntity> sortBuilder
) =>
(await queryable.Where(queryBuilder.Build())
.ApplyOrdering(sortBuilder)
.ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token));
public static async Task<List<SlotBase>> GetSlots
(
this DatabaseContext database,
GameTokenEntity token,
SlotQueryBuilder queryBuilder,
PaginationData pageData,
ISortBuilder<SlotEntity> sortBuilder
) =>
(await database.Slots.Where(queryBuilder.Build())
.ApplyOrdering(sortBuilder)
.ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token));
public static async Task<List<SlotBase>> GetSlots
(
this DatabaseContext database,
GameTokenEntity token,
SlotQueryBuilder queryBuilder,
PaginationData pageData,
ISortBuilder<SlotMetadata> sortBuilder,
Expression<Func<SlotEntity, SlotMetadata>> 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));
}

View file

@ -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());

View file

@ -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<SlotEntity> GetSlots(DatabaseContext database, UserEntity user, int pageStart, int pageSize);
public override IList<SlotEntity> 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<SlotEntity>();
}
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<SlotBase> 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));
}
}

View file

@ -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<int> SlotIds;
private readonly List<int> slotIds;
public CustomCategory(string name, string description, string endpoint, string icon, IEnumerable<int> 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<SlotEntity> 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<SlotEntity> GetItems(DatabaseContext database, GameTokenEntity entity, SlotQueryBuilder queryBuilder)
{
queryBuilder.Clone().AddFilter(new SlotIdFilter(this.slotIds));
return database.Slots.Where(queryBuilder.Build());
}
}

View file

@ -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<SlotEntity> 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<SlotEntity> 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());
}

View file

@ -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<SlotEntity> 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<SlotEntity> 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());
}

View file

@ -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<SlotEntity> 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<SlotEntity> GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) =>
database.Slots.Where(queryBuilder.Build())
.ApplyOrdering(new SlotSortBuilder<SlotEntity>().AddSort(new FirstUploadedSort()));
}

View file

@ -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<SlotEntity> 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<SlotMetadata>().AddSort(new HeartsSort()))
.Select(s => s.Slot)
.FirstOrDefault();
public override IEnumerable<SlotEntity> 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());
}

View file

@ -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<SlotEntity> 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<SlotEntity> 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);
}

View file

@ -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<UserEntity> 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);
}

View file

@ -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<SlotEntity> 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<SlotEntity> GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) =>
database.Slots.Where(queryBuilder.Build())
.ApplyOrdering(new SlotSortBuilder<SlotEntity>().AddSort(new FirstUploadedSort()));
}

View file

@ -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<PlaylistEntity> GetItems(DatabaseContext database, GameTokenEntity token);
public override async Task<GameCategory> Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1)
{
List<ILbpSerializable> playlists =
(await this.GetItems(database, token).Take(numResults).ToListAsync())
.ToSerializableList<PlaylistEntity, ILbpSerializable>(GamePlaylist.CreateFromEntity);
int totalPlaylists = await this.GetItems(database, token).CountAsync();
return GameCategory.CreateFromEntity(this, new GenericSerializableList(playlists, totalPlaylists, numResults + 1));
}
}

View file

@ -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<SlotEntity> 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<SlotEntity> 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());
}

View file

@ -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<SlotEntity> GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder);
public override async Task<GameCategory> Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1)
{
List<ILbpSerializable> slots =
(await this.GetItems(database, token, queryBuilder).Take(numResults).ToListAsync())
.ToSerializableList<SlotEntity, ILbpSerializable>(s => SlotBase.CreateFromEntity(s, token));
int totalSlots = await this.GetItems(database, token, queryBuilder).CountAsync();
return GameCategory.CreateFromEntity(this, new GenericSerializableList(slots, totalSlots, numResults+1));
}
}

View file

@ -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<SlotEntity> 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<SlotEntity> GetItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder) =>
database.Slots.Where(queryBuilder.Clone().AddFilter(new TeamPickFilter()).Build())
.ApplyOrdering(new SlotSortBuilder<SlotEntity>().AddSort(new FirstUploadedSort()));
}

View file

@ -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<UserEntity> GetItems(DatabaseContext database, GameTokenEntity token);
public override async Task<GameCategory> Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1)
{
List<ILbpSerializable> users =
(await this.GetItems(database, token).Take(numResults).ToListAsync())
.ToSerializableList<UserEntity, ILbpSerializable>(GameUser.CreateFromEntity);
int totalUsers = await this.GetItems(database, token).CountAsync();
return GameCategory.CreateFromEntity(this, new GenericSerializableList(users, totalUsers, numResults + 1));
}
}

View file

@ -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<PlaylistEntity> GetItems(DatabaseContext database, GameTokenEntity token) =>
database.Playlists.Where(p => p.CreatorId == token.UserId).OrderByDescending(p => p.PlaylistId);
}

View file

@ -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"));

View file

@ -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<GameServerTestStartup>
{
[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("<id>23</id>", 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<GameUserSlotList>(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<GameUserSlotList>(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<GameUserSlotList>(deserialized);
GameUserSlotList slotResponse = (GameUserSlotList)deserialized;
Assert.Empty(slotResponse.Slots);
}
[XmlRoot("slots")]
public class GameUserSlotList
{
[XmlElement("slot")]
public List<GameUserSlot> Slots { get; set; } = new();
[XmlAttribute("total")]
public int Total { get; set; }
[XmlAttribute("hint_start")]
public int HintStart { get; set; }
}
}

View file

@ -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<ISlotFilter> 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)));
}
}

View file

@ -41,11 +41,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\n";
IActionResult result = messageController.Eula();
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expected, (string)okObjectResult.Value);
string eulaMsg = result.CastTo<OkObjectResult, string>();
Assert.Equal(expected, eulaMsg);
}
[Fact]
@ -72,11 +69,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
IActionResult result = messageController.Eula();
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expected, (string)okObjectResult.Value);
string eulaMsg = result.CastTo<OkObjectResult, string>();
Assert.Equal(expected, eulaMsg);
}
[Fact]
@ -92,11 +86,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
IActionResult result = await messageController.Announce();
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expected, (string)okObjectResult.Value);
string announceMsg = result.CastTo<OkObjectResult, string>();
Assert.Equal(expected, announceMsg);
}
[Fact]
@ -112,11 +103,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
IActionResult result = await messageController.Announce();
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expected, (string)okObjectResult.Value);
string announceMsg = result.CastTo<OkObjectResult, string>();
Assert.Equal(expected, announceMsg);
}
[Fact]
@ -147,11 +135,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
IActionResult result = await messageController.Filter(new NullMailService());
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expectedBody, (string)okObjectResult.Value);
string filteredMessage = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedBody, filteredMessage);
}
[Fact]
@ -173,11 +158,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
IActionResult result = await messageController.Filter(new NullMailService());
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expectedBody, (string)okObjectResult.Value);
string filteredMessage = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedBody, filteredMessage);
}
private static Mock<IMailService> getMailServiceMock()
@ -189,7 +171,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
}
[Fact]
public async void Filter_ShouldNotSendEmail_WhenMailDisabled()
public async Task Filter_ShouldNotSendEmail_WhenMailDisabled()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
Mock<IMailService> mailMock = getMailServiceMock();
@ -200,20 +182,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
ServerConfiguration.Instance.Mail.MailEnabled = false;
CensorConfiguration.Instance.FilteredWordList = new List<string>();
const int expectedStatus = 200;
const string expected = "/setemail unittest@unittest.com";
IActionResult result = await messageController.Filter(mailMock.Object);
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.Equal(expectedStatus, okObjectResult.StatusCode);
Assert.Equal(expected, okObjectResult.Value);
string filteredMessage = result.CastTo<OkObjectResult, string>();
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 <https://www.gnu.org/licenses/>." + "\nuni
}
[Fact]
public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken()
public async Task Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken()
{
List<UserEntity> users = new()
{
@ -265,7 +243,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\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 <https://www.gnu.org/licenses/>." + "\nuni
}
[Fact]
public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailFormatInvalid()
public async Task Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailFormatInvalid()
{
UserEntity unitTestUser = MockHelper.GetUnitTestUser();
unitTestUser.EmailAddressVerified = true;

View file

@ -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<NotFoundResult>(result);
}
[Fact]
public async Task SlotsBy_ShouldFetchLevelsByUser()
{
List<SlotEntity> slots = new()
{
new SlotEntity
{
SlotId = 1,
CreatorId = 2,
},
new SlotEntity
{
SlotId = 2,
CreatorId = 2,
},
new SlotEntity
{
SlotId = 3,
CreatorId = 3,
},
};
List<UserEntity> 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<OkObjectResult, GenericSlotResponse>();
Assert.Equal(expectedElements, slotResponse.Slots.Count);
}
[Fact]
public async Task SlotsBy_ResultsAreOrderedByFirstUploadedTimestampDescending()
{
List<SlotEntity> 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<UserEntity> 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<OkObjectResult, GenericSlotResponse>();
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<SlotEntity> 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<OkObjectResult>(result);
}
[Fact]
public async Task UserSlot_ShouldNotFetch_WhenGameVersionMismatch()
{
List<SlotEntity> 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<NotFoundResult>(result);
}
[Fact]
public async Task UserSlot_ShouldFetch_WhenGameVersionEqual()
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = GameVersion.LittleBigPlanetVita;
List<GameTokenEntity> tokens = new()
{
token,
};
List<SlotEntity> 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<OkObjectResult>(result);
}
[Fact]
public async Task UserSlot_ShouldFetch_WhenGameVersionIsGreater()
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = GameVersion.LittleBigPlanet3;
List<GameTokenEntity> tokens = new()
{
token,
};
List<SlotEntity> 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<OkObjectResult>(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<NotFoundResult>(result);
}
[Fact]
public async Task UserSlot_ShouldFetch_WhenSlotIsNotSubLevel()
{
List<SlotEntity> 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<OkObjectResult>(result);
}
[Fact]
public async Task UserSlot_ShouldNotFetch_WhenSlotIsHidden()
{
List<SlotEntity> 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<NotFoundResult>(result);
}
[Fact]
public async Task UserSlot_ShouldNotFetch_WhenSlotIsWrongType()
{
List<SlotEntity> 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<NotFoundResult>(result);
}
[Fact]
public async Task UserSlot_ShouldNotFetch_WhenSlotIsSubLevel()
{
List<SlotEntity> 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<NotFoundResult>(result);
}
#endregion
#region DeveloperSlot
[Fact]
public async Task DeveloperSlot_ShouldFetch_WhenSlotIdIsValid()
{
List<SlotEntity> 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<OkObjectResult>(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<OkObjectResult>(result);
}
#endregion
}

View file

@ -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<OkObjectResult>(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<OkObjectResult, PlanetStatsResponse>();
Assert.Equal(expectedSlots, statsResponse.TotalSlotCount);
Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount);
}
[Fact]
public async void PlanetStats_ShouldReturnCorrectCounts_WhenNotEmpty()
public async Task PlanetStats_ShouldReturnCorrectCounts_WhenNotEmpty()
{
List<SlotEntity> slots = new()
{
@ -64,17 +61,13 @@ public class StatisticsControllerTests
IActionResult result = await statsController.PlanetStats();
Assert.IsType<OkObjectResult>(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<OkObjectResult, PlanetStatsResponse>();
Assert.Equal(expectedSlots, statsResponse.TotalSlotCount);
Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount);
}
[Fact]
public async void PlanetStats_ShouldReturnCorrectCounts_WhenSlotsAreIncompatibleGameVersion()
public async Task PlanetStats_ShouldReturnCorrectCounts_WhenSlotsAreIncompatibleGameVersion()
{
List<SlotEntity> slots = new()
{
@ -105,17 +98,13 @@ public class StatisticsControllerTests
IActionResult result = await statsController.PlanetStats();
Assert.IsType<OkObjectResult>(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<OkObjectResult, PlanetStatsResponse>();
Assert.Equal(expectedSlots, statsResponse.TotalSlotCount);
Assert.Equal(expectedTeamPicks, statsResponse.TeamPickCount);
}
[Fact]
public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreCompatible()
public async Task TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreCompatible()
{
List<SlotEntity> slots = new()
{
@ -145,14 +134,12 @@ public class StatisticsControllerTests
IActionResult result = await statsController.TotalLevelCount();
Assert.IsType<OkObjectResult>(result);
OkObjectResult? objectResult = result as OkObjectResult;
Assert.NotNull(objectResult);
Assert.Equal(expectedTotal, objectResult.Value);
string totalSlotsResponse = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedTotal, totalSlotsResponse);
}
[Fact]
public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible()
public async Task TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible()
{
List<SlotEntity> 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<OkObjectResult>(result);
OkObjectResult? objectResult = result as OkObjectResult;
Assert.NotNull(objectResult);
Assert.Equal(expectedStatusCode, objectResult.StatusCode);
Assert.Equal(expectedTotal, objectResult.Value);
string totalSlots = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedTotal, totalSlots);
}
}

View file

@ -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<OkObjectResult>(result);
OkObjectResult? okObject = result as OkObjectResult;
Assert.NotNull(okObject);
GameUser? gameUser = okObject.Value as GameUser;
Assert.NotNull(gameUser);
GameUser gameUser = result.CastTo<OkObjectResult, GameUser>();
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<OkObjectResult>(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<OkObjectResult, MinimalUserListResponse>();
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<OkObjectResult>(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<OkObjectResult, MinimalUserListResponse>();
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<OkObjectResult>(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<OkObjectResult, MinimalUserListResponse>();
Assert.Single(userList.Users);
}
[Fact]
public async void GetUserAlt_WithTwoValidUsers_ShouldReturnTwo()
public async Task GetUserAlt_WithTwoValidUsers_ShouldReturnTwo()
{
List<UserEntity> users = new()
{
@ -132,16 +117,12 @@ public class UserControllerTests
"unittest2", "unittest",
});
Assert.IsType<OkObjectResult>(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<OkObjectResult, MinimalUserListResponse>();
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<OkObjectResult>(result);
OkObjectResult? okObject = result as OkObjectResult;
Assert.NotNull(okObject);
string pinsResponse = result.CastTo<OkObjectResult, string>();
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<OkObjectResult>(result);
OkObjectResult? okObject = result as OkObjectResult;
Assert.NotNull(okObject);
string pinsResponse = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedPins, dbMock.Users.First().Pins);
Assert.Equal(expectedResponse, okObject.Value);
Assert.Equal(expectedResponse, pinsResponse);
}
}

View file

@ -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()
{

View file

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

View file

@ -8,7 +8,6 @@ namespace LBPUnion.ProjectLighthouse.Tests.Helpers;
public static class IntegrationHelper
{
private static readonly Lazy<bool> dbConnected = new(IsDbConnected);
private static bool IsDbConnected() => ServerStatics.DbConnected;

View file

@ -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<T1, T2>(this IActionResult result) where T1 : ObjectResult
{
Assert.IsType<T1>(result);
T1? typedResult = result as T1;
Assert.NotNull(typedResult);
Assert.NotNull(typedResult.Value);
Assert.IsType<T2?>(typedResult.Value);
T2? finalResult = (T2?)typedResult.Value;
Assert.NotNull(finalResult);
return finalResult;
}
public static async Task<DatabaseContext> GetTestDatabase(IEnumerable<IList> sets, [CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0)
{
Dictionary<Type, IList> 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() =>

View file

@ -88,6 +88,8 @@ public class LighthouseServerTest<TStartup> where TStartup : class
private Task<HttpResponseMessage> 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];

View file

@ -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<SlotEntity, bool> adventureFunc = adventureFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
IsAdventurePlanet = true,
};
Assert.True(adventureFunc(slot));
}
[Fact]
public void AdventureFilter_ShouldReject_WhenNotAdventure()
{
AdventureFilter adventureFilter = new();
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<string>();
AuthorLabelFilter labelFilter = new(filters);
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> creatorFunc = creatorFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
CreatorId = slotCreatorId,
};
Assert.False(creatorFunc(slot));
}
[Fact]
public void CrossControlFilter_ShouldAccept_WhenCrossControlRequired()
{
CrossControlFilter crossControlFilter = new();
Func<SlotEntity, bool> ccFunc = crossControlFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
CrossControllerRequired = true,
};
Assert.True(ccFunc(slot));
}
[Fact]
public void CrossControlFilter_ShouldReject_WhenCrossControlNotRequired()
{
CrossControlFilter crossControlFilter = new();
Func<SlotEntity, bool> ccFunc = crossControlFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
CrossControllerRequired = false,
};
Assert.False(ccFunc(slot));
}
[Fact]
public void ExcludeAdventureFilter_ShouldReject_WhenAdventure()
{
ExcludeAdventureFilter excludeAdventureFilter = new();
Func<SlotEntity, bool> adventureFunc = excludeAdventureFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
IsAdventurePlanet = true,
};
Assert.False(adventureFunc(slot));
}
[Fact]
public void ExcludeAdventureFilter_ShouldAccept_WhenNotAdventure()
{
ExcludeAdventureFilter excludeAdventureFilter = new();
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> excludeFunc = excludeMove.GetPredicate().Compile();
SlotEntity slot = new()
{
MoveRequired = true,
};
Assert.False(excludeFunc(slot));
}
[Fact]
public void ExcludeMovePackFilter_ShouldAccept_WhenMoveNotRequired()
{
ExcludeMovePackFilter excludeMove = new();
Func<SlotEntity, bool> excludeFunc = excludeMove.GetPredicate().Compile();
SlotEntity slot = new()
{
MoveRequired = false,
};
Assert.True(excludeFunc(slot));
}
[Fact]
public void FirstUploadedFilter_ShouldReject_WhenOlderThanStartTime()
{
FirstUploadedFilter uploadFilter = new(1000);
Func<SlotEntity, bool> uploadFunc = uploadFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
FirstUploaded = 999,
};
Assert.False(uploadFunc(slot));
}
[Fact]
public void FirstUploadedFilter_ShouldAccept_WhenNewerThanStartTime()
{
FirstUploadedFilter uploadFilter = new(1000);
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> versionFunc = gameVersionListFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
GameVersion = GameVersion.LittleBigPlanet1,
};
Assert.True(versionFunc(slot));
}
[Fact]
public void HiddenSlotFilter_ShouldReject_WhenHidden()
{
HiddenSlotFilter hiddenSlotFilter = new();
Func<SlotEntity, bool> hiddenFunc = hiddenSlotFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
Hidden = true,
};
Assert.False(hiddenFunc(slot));
}
[Fact]
public void HiddenSlotFilter_ShouldAccept_WhenNotHidden()
{
HiddenSlotFilter hiddenSlotFilter = new();
Func<SlotEntity, bool> hiddenFunc = hiddenSlotFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
Hidden = false,
};
Assert.True(hiddenFunc(slot));
}
[Fact]
public void MoveFilter_ShouldAccept_WhenMoveRequired()
{
MovePackFilter movePackFilter = new();
Func<SlotEntity, bool> moveFunc = movePackFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
MoveRequired = true,
};
Assert.True(moveFunc(slot));
}
[Fact]
public void MoveFilter_ShouldReject_WhenMoveNotRequired()
{
MovePackFilter movePackFilter = new();
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> resultFunc = resultFilter.GetPredicate().Compile();
SlotEntity slot = new();
Assert.False(resultFunc(slot));
}
[Fact]
public void ResultTypeFilter_ShouldAccept_WhenSlotPresent()
{
ResultTypeFilter resultFilter = new("slot");
Func<SlotEntity, bool> resultFunc = resultFilter.GetPredicate().Compile();
SlotEntity slot = new();
Assert.True(resultFunc(slot));
}
[Fact]
public void SlotIdFilter_ShouldReject_WhenSlotIdNotPresent()
{
SlotIdFilter idFilter = new(new List<int>
{
2,
});
Func<SlotEntity, bool> idFunc = idFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
SlotId = 1,
};
Assert.False(idFunc(slot));
}
[Fact]
public void SlotIdFilter_ShouldAccept_WhenSlotIdPresent()
{
SlotIdFilter idFilter = new(new List<int>
{
2,
});
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> teamPickFunc = teamPickFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
TeamPick = true,
};
Assert.True(teamPickFunc(slot));
}
[Fact]
public void TeamPickFilter_ShouldReject_WhenNotTeamPick()
{
TeamPickFilter teamPickFilter = new();
Func<SlotEntity, bool> teamPickFunc = teamPickFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
TeamPick = false,
};
Assert.False(teamPickFunc(slot));
}
[Fact]
public void TextFilter_ShouldAccept_WhenDescriptionContainsText()
{
TextFilter textFilter = new("test");
Func<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> 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<SlotEntity, bool> textFunc = textFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
SlotId = 21,
};
Assert.True(textFunc(slot));
}
[Fact]
public void TextFilter_ShouldReject_WhenIdDoesNotContainText()
{
TextFilter textFilter = new("21");
Func<SlotEntity, bool> textFunc = textFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
SlotId = 19,
};
Assert.False(textFunc(slot));
}
[Fact]
public void TextFilter_ShouldAccept_WhenCreatorUsernameContainsText()
{
TextFilter textFilter = new("test");
Func<SlotEntity, bool> 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<SlotEntity, bool> textFunc = textFilter.GetPredicate().Compile();
SlotEntity slot = new()
{
Creator = new UserEntity
{
Username = "bruh",
},
};
Assert.False(textFunc(slot));
}
}

View file

@ -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<string, StringValues>
{
{
"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<string, StringValues>
{
{
"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<string, StringValues>
{
{
"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<string, StringValues>
{
{
"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<GameUser> users = new();
for (int i = 0; i < 30; i++)
{
users.Add(new GameUser
{
UserId = i+1,
});
}
PaginationData pageData = new()
{
PageSize = 5,
PageStart = 6,
};
List<GameUser> 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<GameUser> users = new();
for (int i = 0; i < 30; i++)
{
users.Add(new GameUser
{
UserId = i + 1,
});
}
PaginationData pageData = new()
{
PageSize = 5,
PageStart = -5,
};
List<GameUser> 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<GameUser> users = new();
for (int i = 0; i < 30; i++)
{
users.Add(new GameUser
{
UserId = i + 1,
});
}
PaginationData pageData = new()
{
PageSize = -5,
PageStart = 0,
};
List<GameUser> pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList();
Assert.Empty(pagedUsers);
}
[Fact]
public void ApplyPagination_ShouldClampPageSize_WhenSizeExceedsMaxElements()
{
List<GameUser> 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<GameUser> pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList();
Assert.Single(pagedUsers);
}
[Fact]
public void ApplyPagination_ShouldClampPageSize_WhenSizeExceedsInternalLimit()
{
List<GameUser> 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<GameUser> pagedUsers = users.AsQueryable().ApplyPagination(pageData).ToList();
Assert.Equal(1000, pagedUsers.Count);
}
}

View file

@ -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);

View file

@ -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<SlotEntity> ByGameVersion
(this DbSet<SlotEntity> set, GameVersion gameVersion, bool includeSublevels = false, bool includeCreator = false)
=> set.AsQueryable().ByGameVersion(gameVersion, includeSublevels, includeCreator);
public static IQueryable<SlotEntity> ByGameVersion
(this IQueryable<SlotEntity> 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<ReviewEntity> ByGameVersion(this IQueryable<ReviewEntity> queryable, GameVersion gameVersion, bool includeSublevels = false)
{
IQueryable<ReviewEntity> 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<bool> Has<T>(this IQueryable<T> queryable, Expression<Func<T, bool>> predicate)
=> await queryable.FirstOrDefaultAsync(predicate) != null;

View file

@ -0,0 +1,30 @@
using System;
using System.Linq.Expressions;
namespace LBPUnion.ProjectLighthouse.Extensions;
public static class PredicateExtensions
{
public static Expression<Func<T, bool>> True<T>()
{
return f => true;
}
public static Expression<Func<T, bool>> False<T>()
{
return f => false;
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
InvocationExpression invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>
(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
InvocationExpression invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
}
}

View file

@ -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<T2> ToSerializableList<T, T2>(this IEnumerable<T> enumerable, Func<T, T2> selector)
=> enumerable.Select(selector).ToList();
public static IQueryable<T> ApplyPagination<T>(this IQueryable<T> 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<T> ApplyOrdering<T>
(this IQueryable<T> queryable, ISortBuilder<T> sortBuilder) =>
sortBuilder.Build(queryable);
}

View file

@ -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

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.IsAdventurePlanet);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
predicate = this.labels.Aggregate(predicate,
(current, label) => current.And(s => s.AuthorLabels.Contains(label)));
return predicate;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.CreatorId == this.creatorId);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.CrossControllerRequired);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => !s.IsAdventurePlanet);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
predicate = predicate.And(s => !s.Lbp1Only || s.CreatorId == this.userId || this.targetGameVersion == GameVersion.LittleBigPlanet1);
return predicate;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => !s.MoveRequired);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
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;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
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;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
this.versions.Aggregate(PredicateExtensions.False<SlotEntity>(),
(current, version) => current.Or(s => s.GameVersion == version));
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => !s.Hidden);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.MoveRequired);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
predicate = predicate.And(s => s.MinimumPlayers >= this.minPlayers);
predicate = predicate.And(s => s.MaximumPlayers <= this.maxPlayers);
return predicate;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
this.results.Contains("slot")
? PredicateExtensions.True<SlotEntity>()
: PredicateExtensions.False<SlotEntity>();
}

View file

@ -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<int> slotIds;
public SlotIdFilter(List<int> slotIds)
{
this.slotIds = slotIds;
}
public Expression<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.False<SlotEntity>();
predicate = this.slotIds.Aggregate(predicate, (current, slotId) => current.Or(s => s.SlotId == slotId));
return predicate;
}
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.Type == this.slotType);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => !s.SubLevel || s.CreatorId == this.userId);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate() =>
PredicateExtensions.True<SlotEntity>().And(s => s.TeamPick);
}

View file

@ -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<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.False<SlotEntity>();
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;
}
}

View file

@ -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<SlotEntity>
{
private readonly List<ISlotFilter> filters;
public SlotQueryBuilder()
{
this.filters = new List<ISlotFilter>();
}
public Expression<Func<SlotEntity, bool>> Build()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
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<ISlotFilter> 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;
}
}

View file

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Linq;
using LBPUnion.ProjectLighthouse.Types.Filter.Sorts;
namespace LBPUnion.ProjectLighthouse.Filter;
public class SlotSortBuilder<T> : ISortBuilder<T>
{
private readonly List<ISort<T>> sorts;
private bool sortDescending;
public SlotSortBuilder()
{
this.sorts = new List<ISort<T>>();
this.sortDescending = true;
}
public SlotSortBuilder<T> AddSort(ISort<T> sort)
{
this.sorts.Add(sort);
return this;
}
public SlotSortBuilder<T> SortDescending(bool descending)
{
this.sortDescending = descending;
return this;
}
public IOrderedQueryable<T> Build(IQueryable<T> queryable)
{
IOrderedQueryable<T> orderedQueryable = (IOrderedQueryable<T>)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<T> 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;
}
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => s.FirstUploaded;
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => s.LastUpdated;
}

View file

@ -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<SlotMetadata>
{
public Expression<Func<SlotMetadata, dynamic>> GetExpression() => s => s.Hearts;
}

View file

@ -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<SlotMetadata>
{
public Expression<Func<SlotMetadata, dynamic>> GetExpression() => s => s.RatingLbp1;
}

View file

@ -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<SlotMetadata>
{
public Expression<Func<SlotMetadata, dynamic>> GetExpression() => s => s.ThumbsUp;
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => EF.Property<dynamic>(s, $"Plays{this.GetColName()}");
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() =>
s => EF.Functions.Random() * (s.FirstUploaded * biasFactor);
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => _ => EF.Functions.Random();
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => s.SlotId;
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => s.PlaysLBP1 + s.PlaysLBP2 + s.PlaysLBP3;
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() => s => EF.Property<dynamic>(s, $"Plays{this.GetColName()}Unique");
}

View file

@ -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<Func<SlotEntity, dynamic>> GetExpression() =>
s => s.PlaysLBP1Unique + s.PlaysLBP2Unique + s.PlaysLBP3Unique;
}

View file

@ -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<int> 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();
}
}
}

View file

@ -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<int> RecentMatchesForGame(DatabaseContext database, GameVersion gameVersion)
=> await database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300 && l.GameVersion == gameVersion).CountAsync();
public static async Task<int> SlotCount(DatabaseContext database) => await database.Slots.Where(s => s.Type == SlotType.User).CountAsync();
public static async Task<int> SlotCountForGame(DatabaseContext database, GameVersion gameVersion, bool includeSublevels = false) => await database.Slots.ByGameVersion(gameVersion, includeSublevels).CountAsync();
public static async Task<int> SlotCount(DatabaseContext database, SlotQueryBuilder queryBuilder) => await database.Slots.Where(queryBuilder.Build()).CountAsync();
public static async Task<int> 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<int> TeamPickCount(DatabaseContext database) => await database.Slots.CountAsync(s => s.TeamPick);
public static async Task<int> 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<int> PhotoCount(DatabaseContext database) => await database.Photos.CountAsync();
#region Moderator/Admin specific

View file

@ -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)
{

View file

@ -0,0 +1,9 @@
using System;
using System.Linq.Expressions;
namespace LBPUnion.ProjectLighthouse.Types.Filter;
public interface IFilter<T>
{
public Expression<Func<T, bool>> GetPredicate();
}

View file

@ -0,0 +1,9 @@
using System;
using System.Linq.Expressions;
namespace LBPUnion.ProjectLighthouse.Types.Filter;
public interface IQueryBuilder<T>
{
public Expression<Func<T, bool>> Build();
}

View file

@ -0,0 +1,6 @@
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Filter;
public interface ISlotFilter : IFilter<SlotEntity>
{ }

View file

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

View file

@ -0,0 +1,6 @@
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts;
public interface ISlotSort : ISort<SlotEntity>
{ }

View file

@ -0,0 +1,9 @@
using System;
using System.Linq.Expressions;
namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts;
public interface ISort<T>
{
public Expression<Func<T, dynamic>> GetExpression();
}

View file

@ -0,0 +1,8 @@
using System.Linq;
namespace LBPUnion.ProjectLighthouse.Types.Filter.Sorts;
public interface ISortBuilder<T>
{
public IOrderedQueryable<T> Build(IQueryable<T> queryable);
}

View file

@ -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<SlotEntity> 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<SlotBase> 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<GameCategory> Serialize(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder, int numResults = 1) =>
Task.FromResult(GameCategory.CreateFromEntity(this, new GenericSerializableList(new List<ILbpSerializable>(), 0, 0)));
}

View file

@ -27,4 +27,5 @@ public enum LogArea
Deserialization,
Email,
Serialization,
Synchronization,
}

View file

@ -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; }
}

View file

@ -8,7 +8,7 @@ public class CategoryListResponse : ILbpSerializable
{
public CategoryListResponse() { }
public CategoryListResponse(List<GameCategory> categories, int total, int hint, int hintStart)
public CategoryListResponse(List<GameCategory> 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; }

View file

@ -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,
};
}

View file

@ -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)

Some files were not shown because too many files have changed in this diff Show more