-
Notifications
You must be signed in to change notification settings - Fork 32
Implement user category API endpoints #922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9dbb9e1
Refactor API categories
Toastbrot236 527864b
Refactor API category endpoints, implement user category API endpoints
Toastbrot236 478db33
Implement user API category tests
Toastbrot236 e420e84
Shorter API category cache duration
Toastbrot236 4e59e0e
Introducing API hacks, have ApiExtendedGameUserResponse extend ApiGam…
Toastbrot236 86c2df6
Fix and expand GetsNewestUsers test
Toastbrot236 b5e8a1f
Update Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs
jvyden File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
Refresh.Interfaces.APIv3/Endpoints/CategoryApiEndpoints.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
jvyden marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| [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)] | ||
jvyden marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| [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; | ||
| } | ||
| } | ||
35 changes: 35 additions & 0 deletions
35
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Categories/ApiCategoryResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()!; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Categories/ApiUserCategoryResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }; | ||
jvyden marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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()!; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.