From 4d55e38fe7dd7c364552929e9cd330b20027d988 Mon Sep 17 00:00:00 2001 From: Jayden Date: Tue, 24 Jan 2023 20:59:50 -0500 Subject: [PATCH] API & Level Categories (#13) * Basic API Implementation * Update Bunkum to 1.0.4 * API endpoints for getting levels * Category system Future-proofs some stuff for when we add LBP3 categories & more LBP2 categories, as well as API support * Add more details to categories, add endpoint for getting categories * Add skip/count parameters for level api * Cleanup usages * Add ByUserLevelCategory * Allow game to use new category system * Add level search category * Match GameRoute instead of ApiRoute in LevelEndpoints I don't know how I almost let that slip by. * Mark workaround as FIXME --- .../Database/RealmDatabaseContext.cs | 19 ++++-- .../Endpoints/Api/LevelApiEndpoints.cs | 34 ++++++++++ .../Endpoints/Api/UserApiEndpoints.cs | 19 ++++++ .../Endpoints/ApiEndpointAttribute.cs | 21 +++++++ .../Endpoints/AutodiscoverEndpoints.cs | 3 +- .../Endpoints/{ => Game}/CommentEndpoints.cs | 13 ++-- .../Handshake/AuthenticationEndpoints.cs | 10 +-- .../{ => Game}/Handshake/LicenseEndpoints.cs | 5 +- .../{ => Game}/Handshake/MetadataEndpoints.cs | 4 +- .../Endpoints/Game/Levels/LevelEndpoints.cs | 36 +++++++++++ .../{ => Game}/Levels/PublishEndpoints.cs | 8 +-- .../Endpoints/{ => Game}/PresenceEndpoints.cs | 4 +- .../Endpoints/{ => Game}/ResourceEndpoints.cs | 4 +- .../Endpoints/{ => Game}/UserEndpoints.cs | 12 ++-- .../{ => Endpoints}/GameEndpointAttribute.cs | 4 +- .../Endpoints/Levels/LevelEndpoints.cs | 63 ------------------- .../Extensions/RequestContextExtensions.cs | 6 +- Refresh.GameServer/Refresh.GameServer.csproj | 2 +- Refresh.GameServer/Types/GameLocation.cs | 8 +-- .../Levels/Categories/ByUserLevelCategory.cs | 27 ++++++++ .../Levels/Categories/CategoryHandler.cs | 21 +++++++ .../Types/Levels/Categories/LevelCategory.cs | 59 +++++++++++++++++ .../Levels/Categories/SearchLevelCategory.cs | 23 +++++++ Refresh.GameServer/Types/Levels/GameLevel.cs | 19 +++--- .../Types/Lists/ResourceList.cs | 1 - Refresh.GameServer/Types/UserData/GameUser.cs | 12 ++-- Refresh.GameServer/Types/UserData/UserPins.cs | 1 - Refresh.GameServer/Types/Visibility.cs | 3 +- 28 files changed, 313 insertions(+), 128 deletions(-) create mode 100644 Refresh.GameServer/Endpoints/Api/LevelApiEndpoints.cs create mode 100644 Refresh.GameServer/Endpoints/Api/UserApiEndpoints.cs create mode 100644 Refresh.GameServer/Endpoints/ApiEndpointAttribute.cs rename Refresh.GameServer/Endpoints/{ => Game}/CommentEndpoints.cs (88%) rename Refresh.GameServer/Endpoints/{ => Game}/Handshake/AuthenticationEndpoints.cs (91%) rename Refresh.GameServer/Endpoints/{ => Game}/Handshake/LicenseEndpoints.cs (84%) rename Refresh.GameServer/Endpoints/{ => Game}/Handshake/MetadataEndpoints.cs (97%) create mode 100644 Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs rename Refresh.GameServer/Endpoints/{ => Game}/Levels/PublishEndpoints.cs (95%) rename Refresh.GameServer/Endpoints/{ => Game}/PresenceEndpoints.cs (95%) rename Refresh.GameServer/Endpoints/{ => Game}/ResourceEndpoints.cs (97%) rename Refresh.GameServer/Endpoints/{ => Game}/UserEndpoints.cs (96%) rename Refresh.GameServer/{ => Endpoints}/GameEndpointAttribute.cs (93%) delete mode 100644 Refresh.GameServer/Endpoints/Levels/LevelEndpoints.cs create mode 100644 Refresh.GameServer/Types/Levels/Categories/ByUserLevelCategory.cs create mode 100644 Refresh.GameServer/Types/Levels/Categories/CategoryHandler.cs create mode 100644 Refresh.GameServer/Types/Levels/Categories/LevelCategory.cs create mode 100644 Refresh.GameServer/Types/Levels/Categories/SearchLevelCategory.cs diff --git a/Refresh.GameServer/Database/RealmDatabaseContext.cs b/Refresh.GameServer/Database/RealmDatabaseContext.cs index dcf04801..8b8f754d 100644 --- a/Refresh.GameServer/Database/RealmDatabaseContext.cs +++ b/Refresh.GameServer/Database/RealmDatabaseContext.cs @@ -7,6 +7,7 @@ using Refresh.GameServer.Types.Levels; using Refresh.GameServer.Types.UserData; using Bunkum.HttpServer.Database; +using MongoDB.Bson; namespace Refresh.GameServer.Database; @@ -78,11 +79,20 @@ public GameUser CreateUser(string username) [Pure] [ContractAnnotation("null => null; notnull => canbenull")] - public GameUser? GetUser(string? username) + public GameUser? GetUserByUsername(string? username) { if (username == null) return null; return this._realm.All().FirstOrDefault(u => u.Username == username); } + + [Pure] + [ContractAnnotation("null => null; notnull => canbenull")] + public GameUser? GetUserByUuid(string? uuid) + { + if (uuid == null) return null; + if(!ObjectId.TryParse(uuid, out ObjectId objectId)) return null; + return this._realm.All().FirstOrDefault(u => u.UserId == objectId); + } public Token GenerateTokenForUser(GameUser user) { @@ -171,11 +181,12 @@ public IEnumerable GetNewestLevels(int count, int skip) => .Skip(skip) .Take(count); + // FIXME: to get this to work with new categories I removed the total number of results, this is terrible [Pure] - public (IEnumerable list, int count) SearchForLevels(int count, int skip, string query) + public IEnumerable SearchForLevels(int count, int skip, string query) { string[] keywords = query.Split(' '); - if (keywords.Length == 0) return (Array.Empty(), 0); + if (keywords.Length == 0) return Array.Empty(); IQueryable levels = this._realm.All(); @@ -190,7 +201,7 @@ public IEnumerable GetNewestLevels(int count, int skip) => ); } - return (levels.AsEnumerable().Skip(skip).Take(count), levels.Count()); + return levels.AsEnumerable().Skip(skip).Take(count); } [Pure] diff --git a/Refresh.GameServer/Endpoints/Api/LevelApiEndpoints.cs b/Refresh.GameServer/Endpoints/Api/LevelApiEndpoints.cs new file mode 100644 index 00000000..82e518fa --- /dev/null +++ b/Refresh.GameServer/Endpoints/Api/LevelApiEndpoints.cs @@ -0,0 +1,34 @@ +using System.Net; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.Levels.Categories; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Endpoints.Api; + +public class LevelApiEndpoints : EndpointGroup +{ + [ApiEndpoint("levels/{route}")] + [Authentication(false)] + [NullStatusCode(HttpStatusCode.NotFound)] + public IEnumerable? GetLevels(RequestContext context, RealmDatabaseContext database, GameUser? user, string route) + => CategoryHandler.Categories + .FirstOrDefault(c => c.ApiRoute.StartsWith(route))? + .Fetch(context, database, user); + + [ApiEndpoint("levels")] + [Authentication(false)] + public IEnumerable GetCategories(RequestContext context) => CategoryHandler.Categories; + + [ApiEndpoint("level/id/{idStr}")] + [Authentication(false)] + public GameLevel? GetLevelById(RequestContext context, RealmDatabaseContext database, string idStr) + { + int.TryParse(idStr, out int id); + if (id == default) return null; + + return database.GetLevelById(id); + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/Api/UserApiEndpoints.cs b/Refresh.GameServer/Endpoints/Api/UserApiEndpoints.cs new file mode 100644 index 00000000..88296054 --- /dev/null +++ b/Refresh.GameServer/Endpoints/Api/UserApiEndpoints.cs @@ -0,0 +1,19 @@ +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Endpoints.Api; + +public class UserApiEndpoints : EndpointGroup +{ + [ApiEndpoint("user/name/{username}")] + [Authentication(false)] + public GameUser? GetUserByName(RequestContext context, RealmDatabaseContext database, string username) + => database.GetUserByUsername(username); + + [ApiEndpoint("user/uuid/{uuid}")] + [Authentication(false)] + public GameUser? GetUserByUuid(RequestContext context, RealmDatabaseContext database, string uuid) + => database.GetUserByUuid(uuid); +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/ApiEndpointAttribute.cs b/Refresh.GameServer/Endpoints/ApiEndpointAttribute.cs new file mode 100644 index 00000000..815a3cb5 --- /dev/null +++ b/Refresh.GameServer/Endpoints/ApiEndpointAttribute.cs @@ -0,0 +1,21 @@ +using Bunkum.HttpServer.Endpoints; +using Bunkum.HttpServer.Responses; +using JetBrains.Annotations; + +namespace Refresh.GameServer.Endpoints; + +[MeansImplicitUse] +public class ApiEndpointAttribute : EndpointAttribute +{ + // v2, since maybe we want to add add v1 for backwards compatibility with project lighthouse? + // LegacyApiEndpointAttribute for lighthouse api + private const string BaseRoute = "/api/v2/"; + + public ApiEndpointAttribute(string route, Method method = Method.Get, ContentType contentType = ContentType.Json) + : base(BaseRoute + route, method, contentType) + {} + + public ApiEndpointAttribute(string route, ContentType contentType, Method method = Method.Get) + : base(BaseRoute + route, contentType, method) + {} +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/AutodiscoverEndpoints.cs b/Refresh.GameServer/Endpoints/AutodiscoverEndpoints.cs index a8b1bcb7..d691eb78 100644 --- a/Refresh.GameServer/Endpoints/AutodiscoverEndpoints.cs +++ b/Refresh.GameServer/Endpoints/AutodiscoverEndpoints.cs @@ -1,9 +1,8 @@ -using Newtonsoft.Json; -using Refresh.GameServer.Configuration; using Bunkum.HttpServer; using Bunkum.HttpServer.Configuration; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Newtonsoft.Json; namespace Refresh.GameServer.Endpoints; diff --git a/Refresh.GameServer/Endpoints/CommentEndpoints.cs b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs similarity index 88% rename from Refresh.GameServer/Endpoints/CommentEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs index 05a39313..f8eefabe 100644 --- a/Refresh.GameServer/Endpoints/CommentEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs @@ -1,22 +1,21 @@ using System.Net; -using JetBrains.Annotations; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Bunkum.HttpServer.Responses; using Refresh.GameServer.Database; using Refresh.GameServer.Extensions; using Refresh.GameServer.Types.Comments; using Refresh.GameServer.Types.Lists; using Refresh.GameServer.Types.UserData; -using Bunkum.HttpServer; -using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; -namespace Refresh.GameServer.Endpoints; +namespace Refresh.GameServer.Endpoints.Game; public class CommentEndpoints : EndpointGroup { [GameEndpoint("postUserComment/{username}", ContentType.Xml)] public Response PostProfileComment(RequestContext context, RealmDatabaseContext database, string username, GameComment body, GameUser user) { - GameUser? profile = database.GetUser(username); + GameUser? profile = database.GetUserByUsername(username); if (profile == null) return new Response(HttpStatusCode.NotFound); database.PostCommentToProfile(profile, user, body.Content); @@ -27,7 +26,7 @@ public Response PostProfileComment(RequestContext context, RealmDatabaseContext [NullStatusCode(HttpStatusCode.NotFound)] public GameCommentList? GetProfileComments(RequestContext context, RealmDatabaseContext database, string username) { - GameUser? profile = database.GetUser(username); + GameUser? profile = database.GetUserByUsername(username); if (profile == null) return null; (int skip, int count) = context.GetPageData(); diff --git a/Refresh.GameServer/Endpoints/Handshake/AuthenticationEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs similarity index 91% rename from Refresh.GameServer/Endpoints/Handshake/AuthenticationEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs index 1e361ca9..baec2b04 100644 --- a/Refresh.GameServer/Endpoints/Handshake/AuthenticationEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs @@ -1,14 +1,14 @@ using System.Net; using System.Xml.Serialization; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Bunkum.HttpServer.Responses; using NPTicket; using Refresh.GameServer.Authentication; using Refresh.GameServer.Database; using Refresh.GameServer.Types.UserData; -using Bunkum.HttpServer; -using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; -namespace Refresh.GameServer.Endpoints.Handshake; +namespace Refresh.GameServer.Endpoints.Game.Handshake; public class AuthenticationEndpoints : EndpointGroup { @@ -28,7 +28,7 @@ public class AuthenticationEndpoints : EndpointGroup return null; } - GameUser? user = database.GetUser(ticket.Username); + GameUser? user = database.GetUserByUsername(ticket.Username); user ??= database.CreateUser(ticket.Username); Token token = database.GenerateTokenForUser(user); diff --git a/Refresh.GameServer/Endpoints/Handshake/LicenseEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/LicenseEndpoints.cs similarity index 84% rename from Refresh.GameServer/Endpoints/Handshake/LicenseEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/Handshake/LicenseEndpoints.cs index ebdeb1a2..98667fb7 100644 --- a/Refresh.GameServer/Endpoints/Handshake/LicenseEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/LicenseEndpoints.cs @@ -1,9 +1,8 @@ -using Refresh.GameServer.Configuration; using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Configuration; -namespace Refresh.GameServer.Endpoints.Handshake; +namespace Refresh.GameServer.Endpoints.Game.Handshake; public class LicenseEndpoints : EndpointGroup { diff --git a/Refresh.GameServer/Endpoints/Handshake/MetadataEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs similarity index 97% rename from Refresh.GameServer/Endpoints/Handshake/MetadataEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs index e89dd4ac..b6a87685 100644 --- a/Refresh.GameServer/Endpoints/Handshake/MetadataEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs @@ -1,10 +1,10 @@ using System.Xml.Serialization; -using Refresh.GameServer.Types; using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Types; -namespace Refresh.GameServer.Endpoints.Handshake; +namespace Refresh.GameServer.Endpoints.Game.Handshake; public class MetadataEndpoints : EndpointGroup { diff --git a/Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs new file mode 100644 index 00000000..aa3730c7 --- /dev/null +++ b/Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs @@ -0,0 +1,36 @@ +using System.Net; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.Levels.Categories; +using Refresh.GameServer.Types.Lists; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Endpoints.Game.Levels; + +public class LevelEndpoints : EndpointGroup +{ + // FIXME: Workaround shitty routing - see https://github.com/LittleBigRefresh/Refresh/pull/13#discussion_r1086131790 for details + [GameEndpoint("slots", ContentType.Xml)] + public GameMinimalLevelList NewestLevels(RequestContext context, RealmDatabaseContext database, GameUser? user) + => this.GetLevels(context, database, user, "newest"); + + [GameEndpoint("slots/{route}", ContentType.Xml)] + public GameMinimalLevelList GetLevels(RequestContext context, RealmDatabaseContext database, GameUser? user, string route) => + new(CategoryHandler.Categories + .FirstOrDefault(c => c.GameRoute.StartsWith(route))? + .Fetch(context, database, user)? + .Select(GameMinimalLevel.FromGameLevel), database.GetTotalLevelCount()); // TODO: proper level count + + [GameEndpoint("s/user/{idStr}", ContentType.Xml)] + [NullStatusCode(HttpStatusCode.NotFound)] + public GameLevel? LevelById(RequestContext context, RealmDatabaseContext database, string idStr) + { + int.TryParse(idStr, out int id); + if (id == default) return null; + + return database.GetLevelById(id); + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/Levels/PublishEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs similarity index 95% rename from Refresh.GameServer/Endpoints/Levels/PublishEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs index b709ba04..640ad7de 100644 --- a/Refresh.GameServer/Endpoints/Levels/PublishEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs @@ -1,12 +1,12 @@ using System.Net; -using Refresh.GameServer.Database; -using Refresh.GameServer.Types.Levels; -using Refresh.GameServer.Types.UserData; using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.UserData; -namespace Refresh.GameServer.Endpoints.Levels; +namespace Refresh.GameServer.Endpoints.Game.Levels; public class PublishEndpoints : EndpointGroup { diff --git a/Refresh.GameServer/Endpoints/PresenceEndpoints.cs b/Refresh.GameServer/Endpoints/Game/PresenceEndpoints.cs similarity index 95% rename from Refresh.GameServer/Endpoints/PresenceEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/PresenceEndpoints.cs index 5800402f..e97dee86 100644 --- a/Refresh.GameServer/Endpoints/PresenceEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/PresenceEndpoints.cs @@ -1,10 +1,10 @@ using System.Xml.Serialization; -using Refresh.GameServer.Database; using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Database; -namespace Refresh.GameServer.Endpoints; +namespace Refresh.GameServer.Endpoints.Game; public class PresenceEndpoints : EndpointGroup { diff --git a/Refresh.GameServer/Endpoints/ResourceEndpoints.cs b/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs similarity index 97% rename from Refresh.GameServer/Endpoints/ResourceEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs index b3185c56..a7c546b6 100644 --- a/Refresh.GameServer/Endpoints/ResourceEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs @@ -1,12 +1,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; -using Refresh.GameServer.Types.Lists; using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Refresh.GameServer.Types.Lists; -namespace Refresh.GameServer.Endpoints; +namespace Refresh.GameServer.Endpoints.Game; public class ResourceEndpoints : EndpointGroup { diff --git a/Refresh.GameServer/Endpoints/UserEndpoints.cs b/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs similarity index 96% rename from Refresh.GameServer/Endpoints/UserEndpoints.cs rename to Refresh.GameServer/Endpoints/Game/UserEndpoints.cs index de2fd545..21afa2c5 100644 --- a/Refresh.GameServer/Endpoints/UserEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs @@ -1,21 +1,21 @@ using System.Net; using System.Xml.Serialization; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Endpoints; +using Bunkum.HttpServer.Responses; using Newtonsoft.Json; using Refresh.GameServer.Database; using Refresh.GameServer.Types.Lists; using Refresh.GameServer.Types.UserData; -using Bunkum.HttpServer; -using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; -namespace Refresh.GameServer.Endpoints; +namespace Refresh.GameServer.Endpoints.Game; public class UserEndpoints : EndpointGroup { [GameEndpoint("user/{name}", Method.Get, ContentType.Xml)] public GameUser? GetUser(RequestContext context, RealmDatabaseContext database, string name) { - GameUser? user = database.GetUser(name); + GameUser? user = database.GetUserByUsername(name); return user; } @@ -29,7 +29,7 @@ public GameUserList GetMultipleUsers(RequestContext context, RealmDatabaseContex foreach (string username in usernames) { - GameUser? user = database.GetUser(username); + GameUser? user = database.GetUserByUsername(username); if (user == null) continue; user.PrepareForSerialization(); diff --git a/Refresh.GameServer/GameEndpointAttribute.cs b/Refresh.GameServer/Endpoints/GameEndpointAttribute.cs similarity index 93% rename from Refresh.GameServer/GameEndpointAttribute.cs rename to Refresh.GameServer/Endpoints/GameEndpointAttribute.cs index 0a825c10..43b1e4b7 100644 --- a/Refresh.GameServer/GameEndpointAttribute.cs +++ b/Refresh.GameServer/Endpoints/GameEndpointAttribute.cs @@ -1,8 +1,8 @@ -using JetBrains.Annotations; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using JetBrains.Annotations; -namespace Refresh.GameServer; +namespace Refresh.GameServer.Endpoints; [MeansImplicitUse] public class GameEndpointAttribute : EndpointAttribute diff --git a/Refresh.GameServer/Endpoints/Levels/LevelEndpoints.cs b/Refresh.GameServer/Endpoints/Levels/LevelEndpoints.cs deleted file mode 100644 index 0723cb5b..00000000 --- a/Refresh.GameServer/Endpoints/Levels/LevelEndpoints.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Net; -using JetBrains.Annotations; -using Refresh.GameServer.Database; -using Refresh.GameServer.Extensions; -using Refresh.GameServer.Types.Levels; -using Refresh.GameServer.Types.Lists; -using Refresh.GameServer.Types.UserData; -using Bunkum.HttpServer; -using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; - -namespace Refresh.GameServer.Endpoints.Levels; - -public class LevelEndpoints : EndpointGroup -{ - [GameEndpoint("slots/by", ContentType.Xml)] - [NullStatusCode(HttpStatusCode.NotFound)] - public GameMinimalLevelList? LevelsByUser(RequestContext context, RealmDatabaseContext database) - { - GameUser? user = database.GetUser(context.Request.QueryString["u"]); - if (user == null) return null; - - (int skip, int count) = context.GetPageData(); - - IEnumerable list = database.GetLevelsByUser(user, count, skip) - .Select(GameMinimalLevel.FromGameLevel); - - return new GameMinimalLevelList(list, database.GetTotalLevelCount()); - } - - [GameEndpoint("slots", ContentType.Xml)] - public GameMinimalLevelList NewestLevels(RequestContext context, RealmDatabaseContext database) - { - (int skip, int count) = context.GetPageData(); - - IEnumerable list = database.GetNewestLevels(count, skip) - .Select(GameMinimalLevel.FromGameLevel); - - return new GameMinimalLevelList(list, database.GetTotalLevelCount()); - } - - [GameEndpoint("slots/search", ContentType.Xml)] - public GameMinimalLevelList SearchForLevels(RequestContext context, RealmDatabaseContext database) - { - (int skip, int count) = context.GetPageData(); - string? query = context.Request.QueryString["query"]; - if (query == null) return new GameMinimalLevelList(); - - (IEnumerable? list, int totalResults) = database.SearchForLevels(count, skip, query); - - return new GameMinimalLevelList(list.Select(GameMinimalLevel.FromGameLevel), totalResults); - } - - [GameEndpoint("s/user/{idStr}", ContentType.Xml)] - [NullStatusCode(HttpStatusCode.NotFound)] - public GameLevel? LevelById(RequestContext context, RealmDatabaseContext database, string idStr) - { - int.TryParse(idStr, out int id); - if (id == default) return null; - - return database.GetLevelById(id); - } -} \ No newline at end of file diff --git a/Refresh.GameServer/Extensions/RequestContextExtensions.cs b/Refresh.GameServer/Extensions/RequestContextExtensions.cs index 2d344409..1ded156c 100644 --- a/Refresh.GameServer/Extensions/RequestContextExtensions.cs +++ b/Refresh.GameServer/Extensions/RequestContextExtensions.cs @@ -6,12 +6,12 @@ namespace Refresh.GameServer.Extensions; public static class RequestContextExtensions { [Pure] - public static (int, int) GetPageData(this RequestContext context) + public static (int, int) GetPageData(this RequestContext context, bool api = false) { - int.TryParse(context.Request.QueryString["pageStart"], out int skip); + int.TryParse(context.Request.QueryString[api ? "skip" : "pageStart"], out int skip); if (skip != default) skip--; - int.TryParse(context.Request.QueryString["pageSize"], out int count); + int.TryParse(context.Request.QueryString[api ? "count" : "pageSize"], out int count); if (count == default) count = 20; return (skip, count); diff --git a/Refresh.GameServer/Refresh.GameServer.csproj b/Refresh.GameServer/Refresh.GameServer.csproj index 3cc2ffa9..8e29517f 100644 --- a/Refresh.GameServer/Refresh.GameServer.csproj +++ b/Refresh.GameServer/Refresh.GameServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Refresh.GameServer/Types/GameLocation.cs b/Refresh.GameServer/Types/GameLocation.cs index e513cf58..cc2348be 100644 --- a/Refresh.GameServer/Types/GameLocation.cs +++ b/Refresh.GameServer/Types/GameLocation.cs @@ -1,9 +1,11 @@ using System.Xml.Serialization; +using Newtonsoft.Json; using Realms; namespace Refresh.GameServer.Types; [XmlType("location")] +[JsonObject(MemberSerialization.OptIn)] public class GameLocation : EmbeddedObject { public static readonly GameLocation Zero = new() @@ -12,8 +14,6 @@ public class GameLocation : EmbeddedObject Y = 0, }; - [XmlElement("y")] - public int X { get; set; } - [XmlElement("x")] - public int Y { get; set; } + [XmlElement("y")] [JsonProperty] public int X { get; set; } + [XmlElement("x")] [JsonProperty] public int Y { get; set; } } \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/ByUserLevelCategory.cs b/Refresh.GameServer/Types/Levels/Categories/ByUserLevelCategory.cs new file mode 100644 index 00000000..b1fab438 --- /dev/null +++ b/Refresh.GameServer/Types/Levels/Categories/ByUserLevelCategory.cs @@ -0,0 +1,27 @@ +using Bunkum.HttpServer; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Levels.Categories; + +public class ByUserLevelCategory : LevelCategory +{ + internal ByUserLevelCategory() : base("byUser", "by", true, nameof(RealmDatabaseContext.GetLevelsByUser)) + { + // Technically this category can apply to any user, but since we fallback to the regular user this name & description still applies + this.Name = "Levels by you"; + this.Description = "A list of levels created by you!"; + this.IconHash = "g820625"; + } + + public override IEnumerable? Fetch(RequestContext context, RealmDatabaseContext database, GameUser? user, object[]? extraArgs = null) + { + // Prefer username from query, but fallback to user passed into this category if it's missing + string? username = context.Request.QueryString["u"]; + if (username != null) user = database.GetUserByUsername(username); + + if (user == null) return null; + + return base.Fetch(context, database, user, extraArgs); + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/CategoryHandler.cs b/Refresh.GameServer/Types/Levels/Categories/CategoryHandler.cs new file mode 100644 index 00000000..eb6e39a8 --- /dev/null +++ b/Refresh.GameServer/Types/Levels/Categories/CategoryHandler.cs @@ -0,0 +1,21 @@ +using Refresh.GameServer.Database; + +namespace Refresh.GameServer.Types.Levels.Categories; + +public static class CategoryHandler +{ + public static IEnumerable Categories => _categories.AsReadOnly(); + + // ReSharper disable once InconsistentNaming + private static readonly List _categories = new() + { + new LevelCategory("newest", "newest", false, nameof(RealmDatabaseContext.GetNewestLevels)) + { + Name = "Newest Levels", + Description = "Levels that were uploaded recently", + IconHash = "g820623", + }, + new ByUserLevelCategory(), + new SearchLevelCategory(), + }; +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/LevelCategory.cs b/Refresh.GameServer/Types/Levels/Categories/LevelCategory.cs new file mode 100644 index 00000000..0f986702 --- /dev/null +++ b/Refresh.GameServer/Types/Levels/Categories/LevelCategory.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using Bunkum.HttpServer; +using Newtonsoft.Json; +using Refresh.GameServer.Database; +using Refresh.GameServer.Extensions; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Levels.Categories; + +[JsonObject(MemberSerialization.OptIn)] +public class LevelCategory +{ + private static readonly Lazy Methods = new(() => typeof(RealmDatabaseContext).GetMethods()); + + [JsonProperty] public string Name { get; set; } = ""; + [JsonProperty] public string Description { get; set; } = ""; + [JsonProperty] public string IconHash { get; set; } = "0"; + + internal LevelCategory(string apiRoute, string gameRoute, bool requiresUser, string funcName) + { + this.ApiRoute = apiRoute; + this.GameRoute = gameRoute; + + this._requiresUser = requiresUser; + + MethodInfo? method = Methods.Value.FirstOrDefault(m => m.Name == funcName); + if (method == null) throw new ArgumentNullException(nameof(funcName), + $"{nameof(funcName)} must point to a method on {nameof(RealmDatabaseContext)}! Use nameof() to assist with this."); + + this._method = method; + } + + [JsonProperty] public readonly string ApiRoute; + public readonly string GameRoute; + + [JsonProperty("RequiresUser")] private readonly bool _requiresUser; + private readonly MethodInfo _method; + + public virtual IEnumerable? Fetch(RequestContext context, RealmDatabaseContext database, GameUser? user, object[]? extraArgs = null) + { + if (this._requiresUser && user == null) return null; + + (int skip, int count) = context.GetPageData(context.Request.Url!.AbsolutePath.StartsWith("/api")); + + IEnumerable args; + + // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression + if (this._requiresUser) +#pragma warning disable CS8601 + args = new object[] { user, count, skip }; +#pragma warning restore CS8601 + else + args = new object[] { count, skip }; + + if (extraArgs != null) args = args.Concat(extraArgs); + + return (IEnumerable)this._method.Invoke(database, args.ToArray())!; + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/SearchLevelCategory.cs b/Refresh.GameServer/Types/Levels/Categories/SearchLevelCategory.cs new file mode 100644 index 00000000..0da4ed38 --- /dev/null +++ b/Refresh.GameServer/Types/Levels/Categories/SearchLevelCategory.cs @@ -0,0 +1,23 @@ +using Bunkum.HttpServer; +using Refresh.GameServer.Database; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Levels.Categories; + +public class SearchLevelCategory : LevelCategory +{ + internal SearchLevelCategory() : base("search", "search", false, nameof(RealmDatabaseContext.SearchForLevels)) + { + // no name/description as this wont ever show up in-game + } + + public override IEnumerable? Fetch(RequestContext context, RealmDatabaseContext database, GameUser? user, object[]? extraArgs = null) + { + string? query = context.Request.QueryString["query"]; + if (query == null) return null; + + extraArgs = new object[] { query }; + + return base.Fetch(context, database, user, extraArgs); + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/GameLevel.cs b/Refresh.GameServer/Types/Levels/GameLevel.cs index e91d7f94..51a02a3c 100644 --- a/Refresh.GameServer/Types/Levels/GameLevel.cs +++ b/Refresh.GameServer/Types/Levels/GameLevel.cs @@ -1,35 +1,36 @@ using System.Xml.Serialization; -using MongoDB.Bson; using Realms; using Refresh.GameServer.Database; using Refresh.GameServer.Types.UserData; using Bunkum.HttpServer.Serialization; +using Newtonsoft.Json; namespace Refresh.GameServer.Types.Levels; [XmlRoot("slot")] [XmlType("slot")] +[JsonObject(MemberSerialization.OptIn)] public class GameLevel : RealmObject, INeedsPreparationBeforeSerialization, ISequentialId { - [PrimaryKey] [Indexed] [XmlElement("id")] public int LevelId { get; set; } + [PrimaryKey] [Indexed] [XmlElement("id")] [JsonProperty] public int LevelId { get; set; } - [XmlElement("name")] public string Title { get; set; } = string.Empty; - [XmlElement("icon")] public string IconHash { get; set; } = string.Empty; - [XmlElement("description")] public string Description { get; set; } = string.Empty; - [XmlElement("location")] public GameLocation Location { get; set; } = GameLocation.Zero; + [XmlElement("name")] [JsonProperty] public string Title { get; set; } = string.Empty; + [XmlElement("icon")] [JsonProperty] public string IconHash { get; set; } = string.Empty; + [XmlElement("description")] [JsonProperty] public string Description { get; set; } = string.Empty; + [XmlElement("location")] [JsonProperty] public GameLocation Location { get; set; } = GameLocation.Zero; [XmlElement("rootLevel")] public string RootResource { get; set; } = string.Empty; [XmlIgnore] public IList Resources { get; } = new List(); - [XmlElement("firstPublished")] public long PublishDate { get; set; } // unix seconds - [XmlElement("lastUpdated")] public long UpdateDate { get; set; } + [XmlElement("firstPublished")] [JsonProperty] public long PublishDate { get; set; } // unix seconds + [XmlElement("lastUpdated")] [JsonProperty] public long UpdateDate { get; set; } public int SequentialId { set => this.LevelId = value; } - public GameUser? Publisher { get; set; } + [JsonProperty] public GameUser? Publisher { get; set; } #region LBP Serialization Quirks [Ignored] [XmlAttribute("type")] public string? Type { get; set; } diff --git a/Refresh.GameServer/Types/Lists/ResourceList.cs b/Refresh.GameServer/Types/Lists/ResourceList.cs index 673dadc8..dfbe8abb 100644 --- a/Refresh.GameServer/Types/Lists/ResourceList.cs +++ b/Refresh.GameServer/Types/Lists/ResourceList.cs @@ -1,5 +1,4 @@ using System.Xml.Serialization; -using Refresh.GameServer.Types.Comments; namespace Refresh.GameServer.Types.Lists; diff --git a/Refresh.GameServer/Types/UserData/GameUser.cs b/Refresh.GameServer/Types/UserData/GameUser.cs index 320c0e4c..cc657d44 100644 --- a/Refresh.GameServer/Types/UserData/GameUser.cs +++ b/Refresh.GameServer/Types/UserData/GameUser.cs @@ -4,18 +4,20 @@ using Refresh.GameServer.Types.Comments; using Bunkum.HttpServer.Authentication; using Bunkum.HttpServer.Serialization; +using Newtonsoft.Json; namespace Refresh.GameServer.Types.UserData; [XmlRoot("user")] +[JsonObject(MemberSerialization.OptIn)] public partial class GameUser : RealmObject, IUser, INeedsPreparationBeforeSerialization { - [PrimaryKey] [Indexed] [XmlIgnore] public ObjectId UserId { get; set; } = ObjectId.GenerateNewId(); - [Indexed] [Required] [XmlIgnore] public string Username { get; set; } = string.Empty; - [XmlIgnore] public string IconHash { get; set; } = "0"; + [PrimaryKey] [Indexed] [XmlIgnore] [JsonProperty] public ObjectId UserId { get; set; } = ObjectId.GenerateNewId(); + [Indexed] [Required] [XmlIgnore] [JsonProperty] public string Username { get; set; } = string.Empty; + [XmlIgnore] [JsonProperty] public string IconHash { get; set; } = "0"; - [XmlElement("biography")] public string Description { get; set; } = ""; - [XmlElement("location")] public GameLocation Location { get; set; } = GameLocation.Zero; + [XmlElement("biography")] [JsonProperty] public string Description { get; set; } = ""; + [XmlElement("location")] [JsonProperty] public GameLocation Location { get; set; } = GameLocation.Zero; [XmlIgnore] public UserPins Pins { get; set; } = new(); diff --git a/Refresh.GameServer/Types/UserData/UserPins.cs b/Refresh.GameServer/Types/UserData/UserPins.cs index fe405b40..ddfc768b 100644 --- a/Refresh.GameServer/Types/UserData/UserPins.cs +++ b/Refresh.GameServer/Types/UserData/UserPins.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Realms; diff --git a/Refresh.GameServer/Types/Visibility.cs b/Refresh.GameServer/Types/Visibility.cs index a6067456..7a5bbd3c 100644 --- a/Refresh.GameServer/Types/Visibility.cs +++ b/Refresh.GameServer/Types/Visibility.cs @@ -1,6 +1,5 @@ -using System.Runtime.Serialization; using System.Xml.Serialization; -using Refresh.GameServer.Endpoints.Handshake; +using Refresh.GameServer.Endpoints.Game.Handshake; namespace Refresh.GameServer.Types;