Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Refresh.Interfaces.APIv3/Endpoints/CategoryApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using AttribDoc.Attributes;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Refresh.Core.Configuration;
using Refresh.Core.Types.Categories;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;
using Refresh.Database.Query;
using Refresh.Interfaces.APIv3.Documentation.Attributes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Categories;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users;
using Refresh.Interfaces.APIv3.Extensions;

namespace Refresh.Interfaces.APIv3.Endpoints;

public class CategoryApiEndpoints : EndpointGroup
{
[ApiV3Endpoint("levels"), Authentication(false)]
[ClientCacheResponse(86400 / 2)] // cache for half a day
[DocSummary("Retrieves a list of categories you can use to search levels")]
[DocQueryParam("includePreviews", "If true, a single level will be added to each category representing a level from that category. False by default.")]
[DocError(typeof(ApiValidationError), "The boolean 'includePreviews' could not be parsed by the server.")]
public ApiListResponse<ApiLevelCategoryResponse> GetLevelCategories(RequestContext context, CategoryService categories,
DataContext dataContext)
{
bool result = bool.TryParse(context.QueryString.Get("includePreviews") ?? "false", out bool includePreviews);
if (!result) return ApiValidationError.BooleanParseError;

IEnumerable<ApiLevelCategoryResponse> resp;

// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (includePreviews) resp = ApiLevelCategoryResponse.FromOldList(categories.LevelCategories, context, dataContext);
else resp = ApiLevelCategoryResponse.FromOldList(categories.LevelCategories, dataContext);

return new ApiListResponse<ApiLevelCategoryResponse>(resp);
}

[ApiV3Endpoint("levels/{route}"), Authentication(false)]
[DocSummary("Retrieves a list of levels from a category")]
[DocError(typeof(ApiNotFoundError), "The level category cannot be found")]
[DocUsesPageData]
[DocQueryParam("game", "Filters levels to a specific game version. Allowed values: lbp1-3, vita, psp, beta")]
[DocQueryParam("seed", "The random seed to use for randomization. Uses 0 if not specified.")]
[DocQueryParam("players", "Filters levels to those accommodating the specified number of players.")]
[DocQueryParam("username", "If set, certain categories like 'hearted' or 'byUser' will return the levels of " +
"the user with this username instead of your own. Optional.")]
public ApiListResponse<ApiGameLevelResponse> GetLevels(RequestContext context, CategoryService categories, GameUser? user,
[DocSummary("The name of the category you'd like to retrieve levels from. " +
"Make a request to /levels to see a list of available categories")]
string route, DataContext dataContext)
{
if (string.IsNullOrWhiteSpace(route))
{
return new ApiError("You didn't specify a route. " +
"You probably meant to use the `/levels` endpoint and left a trailing slash in the URL.", NotFound);
}

(int skip, int count) = context.GetPageData();

DatabaseList<GameLevel>? list = categories.LevelCategories
.FirstOrDefault(c => c.ApiRoute.StartsWith(route))?
.Fetch(context, skip, count, dataContext, new LevelFilterSettings(context, TokenGame.Website), user);

if (list == null) return ApiNotFoundError.Instance;

DatabaseList<ApiGameLevelResponse> levels = DatabaseListExtensions.FromOldList<ApiGameLevelResponse, GameLevel>(list, dataContext);
return levels;
}

[ApiV3Endpoint("users"), Authentication(false)]
[ClientCacheResponse(86400 / 2)] // cache for half a day
[DocSummary("Retrieves a list of categories you can use to search users. Returns an empty list if the instance doesn't allow showing online users.")]
[DocQueryParam("includePreviews", "If true, a single user will be added to each category representing a user from that category. False by default.")]
[DocError(typeof(ApiValidationError), "The boolean 'includePreviews' could not be parsed by the server.")]
public ApiListResponse<ApiUserCategoryResponse> GetUserCategories(RequestContext context, CategoryService categories,
DataContext dataContext, GameServerConfig config)
{
bool result = bool.TryParse(context.QueryString.Get("includePreviews") ?? "false", out bool includePreviews);
if (!result) return ApiValidationError.BooleanParseError;

if (!config.PermitShowingOnlineUsers) return new ApiListResponse<ApiUserCategoryResponse>([]);
IEnumerable<ApiUserCategoryResponse> resp;

if (includePreviews) resp = ApiUserCategoryResponse.FromOldList(categories.UserCategories, context, dataContext);
else resp = ApiUserCategoryResponse.FromOldList(categories.UserCategories, dataContext);

return new ApiListResponse<ApiUserCategoryResponse>(resp);
}

// This route can not be called "users/{route}", else Bunkum will route users/me requests to here aswell.
// Having a special case for the "me" route here would be hacky and introduce trouble if another endpoint with the route
// "users/something" (for example) were to ever be implemented in the future.
[ApiV3Endpoint("users/category/{route}"), Authentication(false)]
[DocSummary("Retrieves a list of users from a category.")]
[DocError(typeof(ApiNotFoundError), "The user category cannot be found, or the instance does not allow showing online users.")]
[DocUsesPageData]
[DocQueryParam("username", "If set, certain categories like 'hearted' will return the related users of " +
"the user with this username instead of your own. Optional.")]
public ApiListResponse<ApiGameUserResponse> GetUsers(RequestContext context, CategoryService categories, GameUser? user,
[DocSummary("The name of the category you'd like to retrieve users from. " +
"Make a request to /users to see a list of available categories")]
string route, DataContext dataContext, GameServerConfig config)
{
if (string.IsNullOrWhiteSpace(route))
{
return new ApiError("You didn't specify a route.", NotFound);
// users/ case won't happen here because of the extra "category" inbetween "users" and the route parameter.
}

if (!config.PermitShowingOnlineUsers) return ApiNotFoundError.Instance;
(int skip, int count) = context.GetPageData();

DatabaseList<GameUser>? list = categories.UserCategories
.FirstOrDefault(c => c.ApiRoute.StartsWith(route))?
.Fetch(context, skip, count, dataContext, user);

if (list == null) return ApiNotFoundError.Instance;

DatabaseList<ApiGameUserResponse> levels = DatabaseListExtensions.FromOldList<ApiGameUserResponse, GameUser>(list, dataContext);
return levels;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Refresh.Core.Types.Categories;
using Refresh.Core.Types.Data;

namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Categories;

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class ApiCategoryResponse : IApiResponse, IDataConvertableFrom<ApiCategoryResponse, GameCategory>
{
public required string Name { get; set; }
public required string Description { get; set; }
public required string IconHash { get; set; }
public required string FontAwesomeIcon { get; set; }
public required string ApiRoute { get; set; }
public required bool RequiresUser { get; set; }
public required bool Hidden { get; set; } = false;

public static ApiCategoryResponse? FromOld(GameCategory? old, DataContext dataContext)
{
if (old == null) return null;

return new ApiCategoryResponse
{
Name = old.Name,
Description = old.Description,
IconHash = old.IconHash,
FontAwesomeIcon = old.FontAwesomeIcon,
ApiRoute = old.ApiRoute,
RequiresUser = old.RequiresUser,
Hidden = old.Hidden,
};
}

public static IEnumerable<ApiCategoryResponse> FromOldList(IEnumerable<GameCategory> oldList, DataContext dataContext)
=> oldList.Select(old => FromOld(old, dataContext)).ToList()!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Query;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;

namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;
namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Categories;

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class ApiLevelCategoryResponse : IApiResponse, IDataConvertableFrom<ApiLevelCategoryResponse, GameLevelCategory>
public class ApiLevelCategoryResponse : ApiCategoryResponse, IApiResponse, IDataConvertableFrom<ApiLevelCategoryResponse, GameLevelCategory>
{
public required string Name { get; set; }
public required string Description { get; set; }
public required string IconHash { get; set; }
public required string FontAwesomeIcon { get; set; }
public required string ApiRoute { get; set; }
public required bool RequiresUser { get; set; }
public required ApiGameLevelResponse? PreviewLevel { get; set; }
public required bool Hidden { get; set; } = false;

public static ApiLevelCategoryResponse? FromOld(GameLevelCategory? old, GameLevel? previewLevel,
DataContext dataContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Bunkum.Core;
using Refresh.Core.Types.Categories.Users;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users;

namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Categories;

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class ApiUserCategoryResponse : ApiCategoryResponse, IApiResponse, IDataConvertableFrom<ApiUserCategoryResponse, GameUserCategory>
{
public required ApiGameUserResponse? PreviewItem { get; set; }

public static ApiUserCategoryResponse? FromOld(GameUserCategory? old, GameUser? PreviewItem,
DataContext dataContext)
{
if (old == null) return null;

return new ApiUserCategoryResponse
{
Name = old.Name,
Description = old.Description,
IconHash = old.IconHash,
FontAwesomeIcon = old.FontAwesomeIcon,
ApiRoute = old.ApiRoute,
RequiresUser = old.RequiresUser,
PreviewItem = ApiGameUserResponse.FromOld(PreviewItem, dataContext),
Hidden = old.Hidden,
};
}

public static ApiUserCategoryResponse? FromOld(GameUserCategory? old, DataContext dataContext)
=> FromOld(old, null, dataContext);

public static IEnumerable<ApiUserCategoryResponse> FromOldList(IEnumerable<GameUserCategory> oldList, DataContext dataContext)
=> oldList.Select(old => FromOld(old, dataContext)).ToList()!;

public static IEnumerable<ApiUserCategoryResponse> FromOldList(IEnumerable<GameUserCategory> oldList,
RequestContext context,
DataContext dataContext)
{
return oldList.Select(category =>
{
DatabaseList<GameUser>? list = category.Fetch(context, 0, 1, dataContext, dataContext.User);
GameUser? item = list?.Items.FirstOrDefault();

return FromOld(category, item, dataContext);
}).ToList()!;
}
}
59 changes: 0 additions & 59 deletions Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,20 @@
using Refresh.Common.Verification;
using Refresh.Core.Authentication.Permission;
using Refresh.Core.Services;
using Refresh.Core.Types.Categories;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Pins;
using Refresh.Database.Models.Users;
using Refresh.Database.Query;
using Refresh.Interfaces.APIv3.Documentation.Attributes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;
using Refresh.Interfaces.APIv3.Extensions;

namespace Refresh.Interfaces.APIv3.Endpoints;

public class LevelApiEndpoints : EndpointGroup
{
[ApiV3Endpoint("levels"), Authentication(false)]
[ClientCacheResponse(86400 / 2)] // cache for half a day
[DocSummary("Retrieves a list of categories you can use to search levels")]
[DocQueryParam("includePreviews", "If true, a single level will be added to each category representing a level from that category. False by default.")]
[DocError(typeof(ApiValidationError), "The boolean 'includePreviews' could not be parsed by the server.")]
public ApiListResponse<ApiLevelCategoryResponse> GetCategories(RequestContext context, CategoryService categories,
MatchService matchService, GameDatabaseContext database, GameUser? user, IDataStore dataStore,
DataContext dataContext)
{
bool result = bool.TryParse(context.QueryString.Get("includePreviews") ?? "false", out bool includePreviews);
if (!result) return ApiValidationError.BooleanParseError;

IEnumerable<ApiLevelCategoryResponse> resp;

// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (includePreviews) resp = ApiLevelCategoryResponse.FromOldList(categories.LevelCategories, context, dataContext);
else resp = ApiLevelCategoryResponse.FromOldList(categories.LevelCategories, dataContext);

return new ApiListResponse<ApiLevelCategoryResponse>(resp);
}

[ApiV3Endpoint("levels/{route}"), Authentication(false)]
[DocSummary("Retrieves a list of levels from a category")]
[DocError(typeof(ApiNotFoundError), "The level category cannot be found")]
[DocUsesPageData]
[DocQueryParam("game", "Filters levels to a specific game version. Allowed values: lbp1-3, vita, psp, beta")]
[DocQueryParam("seed", "The random seed to use for randomization. Uses 0 if not specified.")]
[DocQueryParam("players", "Filters levels to those accommodating the specified number of players.")]
[DocQueryParam("username", "If set, certain categories like 'hearted' or 'byUser' will return the levels of " +
"the user with this username instead of your own. Optional.")]
public ApiListResponse<ApiGameLevelResponse> GetLevels(RequestContext context, GameDatabaseContext database,
MatchService matchService, CategoryService categories, GameUser? user, IDataStore dataStore,
[DocSummary("The name of the category you'd like to retrieve levels from. " +
"Make a request to /levels to see a list of available categories")]
string route, DataContext dataContext)
{
if (string.IsNullOrWhiteSpace(route))
{
return new ApiError("You didn't specify a route. " +
"You probably meant to use the `/levels` endpoint and left a trailing slash in the URL.", NotFound);
}

(int skip, int count) = context.GetPageData();

DatabaseList<GameLevel>? list = categories.LevelCategories
.FirstOrDefault(c => c.ApiRoute.StartsWith(route))?
.Fetch(context, skip, count, dataContext, new LevelFilterSettings(context, TokenGame.Website), user);

if (list == null) return ApiNotFoundError.Instance;

DatabaseList<ApiGameLevelResponse> levels = DatabaseListExtensions.FromOldList<ApiGameLevelResponse, GameLevel>(list, dataContext);
return levels;
}

[ApiV3Endpoint("levels/id/{id}"), Authentication(false)]
[DocSummary("Gets an individual level by a numerical ID")]
[DocError(typeof(ApiNotFoundError), "The level cannot be found")]
Expand Down
1 change: 1 addition & 0 deletions RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Categories;

namespace RefreshTests.GameServer.Tests.ApiV3;

Expand Down
Loading