From 664ace961c9b74f5e85caf7163f902aeff26e158 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 14 Feb 2026 13:48:25 +0100 Subject: [PATCH 1/5] More ApiGameLevelOwnRelationsResponse attributes, include it in level responses --- .../GameDatabaseContext.Relations.cs | 3 ++ .../ApiGameLevelOwnRelationsResponse.cs | 37 +++++++++++++++++++ .../Levels/ApiGameLevelRelationsResponse.cs | 9 ----- .../Response/Levels/ApiGameLevelResponse.cs | 2 + .../Endpoints/LevelApiEndpoints.cs | 9 +++-- 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs delete mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelRelationsResponse.cs diff --git a/Refresh.Database/GameDatabaseContext.Relations.cs b/Refresh.Database/GameDatabaseContext.Relations.cs index 9723bd5b1..6b958cbdc 100644 --- a/Refresh.Database/GameDatabaseContext.Relations.cs +++ b/Refresh.Database/GameDatabaseContext.Relations.cs @@ -650,6 +650,9 @@ public int GetTotalUniquePlaysForLevel(GameLevel level, bool includingAuthor = t public int GetTotalCompletionsForLevel(GameLevel level) => this.GameScores.Count(s => s.Level == level); + + public int GetTotalCompletionsForLevelByUser(GameLevel level, GameUser user) => + this.GameScores.Count(s => s.LevelId == level.LevelId && s.PublisherId == user.UserId); #endregion diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs new file mode 100644 index 000000000..0433ea4fc --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs @@ -0,0 +1,37 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Levels; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiGameLevelOwnRelationsResponse : IApiResponse +{ + public required bool IsHearted { get; set; } + public required bool IsQueued { get; set; } + public required int LevelRating { get; set; } + + /// + /// Returns the total amount of plays; ambiguous name because a unique play count would be nonsense + /// because this is only about one user. Probably rename this in APIv4 anyway. + /// + public required int MyPlaysCount { get; set; } + public required int CompletionCount { get; set; } + public required int PhotoCount { get; set; } + + public static ApiGameLevelOwnRelationsResponse? FromOld(GameLevel level, DataContext dataContext) + { + if (dataContext.User == null) + return null; + + return new() + { + // TODO: Probably cache these stats aswell + IsHearted = dataContext.Database.IsLevelFavouritedByUser(level, dataContext.User), + IsQueued = dataContext.Database.IsLevelQueuedByUser(level, dataContext.User), + LevelRating = (int?)dataContext.Database.GetRatingByUser(level, dataContext.User) ?? 0, + MyPlaysCount = dataContext.Database.GetTotalPlaysForLevelByUser(level, dataContext.User), + CompletionCount = dataContext.Database.GetTotalCompletionsForLevelByUser(level, dataContext.User), + PhotoCount = dataContext.Database.GetTotalPhotosInLevelByUser(level, dataContext.User) + }; + } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelRelationsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelRelationsResponse.cs deleted file mode 100644 index 2f16b6866..000000000 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelRelationsResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels; - -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] -public class ApiGameLevelRelationsResponse : IApiResponse -{ - public required bool IsHearted { get; set; } - public required bool IsQueued { get; set; } - public required int MyPlaysCount { get; set; } -} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs index 7ee20f267..8bf90e61e 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs @@ -55,6 +55,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom Tags { get; set; } + public required ApiGameLevelOwnRelationsResponse? OwnRelations { get; set; } public static ApiGameLevelResponse? FromOld(GameLevel? level, DataContext dataContext) { @@ -102,6 +103,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom GetLevelRelationsOfUser(RequestContext context, GameDatabaseContext database, GameUser user, + public ApiResponse GetLevelRelationsOfUser(RequestContext context, GameDatabaseContext database, GameUser user, [DocSummary("The ID of the level")] int id) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; - return new ApiGameLevelRelationsResponse + return new ApiGameLevelOwnRelationsResponse { IsHearted = database.IsLevelFavouritedByUser(level, user), IsQueued = database.IsLevelQueuedByUser(level, user), - MyPlaysCount = database.GetTotalPlaysForLevelByUser(level, user) + LevelRating = (int?)database.GetRatingByUser(level, user) ?? 0, + MyPlaysCount = database.GetTotalPlaysForLevelByUser(level, user), + CompletionCount = database.GetTotalCompletionsForLevelByUser(level, user), + PhotoCount = database.GetTotalPhotosInLevelByUser(level, user) }; } From ef83560252f739299e18fcc02643a78472c02ede Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 16 Feb 2026 12:37:46 +0100 Subject: [PATCH 2/5] Test own level relations inclusion --- .../Tests/ApiV3/LevelApiTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs index 231803427..10f5daaf9 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs @@ -143,4 +143,23 @@ public async Task CantDeleteLevelIfLevelInvalid() Assert.That(response.StatusCode, Is.EqualTo(NotFound)); Assert.That(context.Database.GetLevelById(id), Is.Not.Null); } + + [Test] + public void OnlyIncludesOwnUserRelationsWhenSignedIn() + { + using TestContext context = this.GetServer(); + GameUser me = context.CreateUser(); + GameLevel level = context.CreateLevel(me); + + // Try fetching the level without being signed in + ApiResponse? response = context.Http.GetData($"/api/v3/levels/id/{level.LevelId}"); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.OwnRelations, Is.Null); + + // Sign in and then get level again + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, me); + response = client.GetData($"/api/v3/levels/id/{level.LevelId}"); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.OwnRelations, Is.Not.Null); + } } \ No newline at end of file From cdb5483702c59b68a6e4d532a31f078aef017ec2 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 16 Feb 2026 13:02:59 +0100 Subject: [PATCH 3/5] Deduplicate ApiGameLevelOwnRelationsResponse initialisation --- .../Endpoints/LevelApiEndpoints.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs index 550a01d23..1d132cc42 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs @@ -132,21 +132,13 @@ public ApiOkResponse SetLevelAsOverrideByHash(RequestContext context, GameDataba [ApiV3Endpoint("levels/id/{id}/relations"), MinimumRole(GameUserRole.Restricted)] [DocSummary("Gets your relations to a level by it's ID")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] - public ApiResponse GetLevelRelationsOfUser(RequestContext context, GameDatabaseContext database, GameUser user, + public ApiResponse GetLevelRelationsOfUser(RequestContext context, DataContext dataContext, GameUser user, [DocSummary("The ID of the level")] int id) { - GameLevel? level = database.GetLevelById(id); + GameLevel? level = dataContext.Database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; - return new ApiGameLevelOwnRelationsResponse - { - IsHearted = database.IsLevelFavouritedByUser(level, user), - IsQueued = database.IsLevelQueuedByUser(level, user), - LevelRating = (int?)database.GetRatingByUser(level, user) ?? 0, - MyPlaysCount = database.GetTotalPlaysForLevelByUser(level, user), - CompletionCount = database.GetTotalCompletionsForLevelByUser(level, user), - PhotoCount = database.GetTotalPhotosInLevelByUser(level, user) - }; + return ApiGameLevelOwnRelationsResponse.FromOld(level, dataContext); } [ApiV3Endpoint("levels/id/{id}/heart", HttpMethods.Post)] From eb14e3a504a787e3458a14df1f38fd485f587c4b Mon Sep 17 00:00:00 2001 From: ToasterTheBrot <95178340+Toastbrot236@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:52:10 +0100 Subject: [PATCH 4/5] Better MyPlaysCount summary Signed-off-by: ToasterTheBrot <95178340+Toastbrot236@users.noreply.github.com> --- .../Response/Levels/ApiGameLevelOwnRelationsResponse.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs index 0433ea4fc..ec74e0de3 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs @@ -11,8 +11,7 @@ public class ApiGameLevelOwnRelationsResponse : IApiResponse public required int LevelRating { get; set; } /// - /// Returns the total amount of plays; ambiguous name because a unique play count would be nonsense - /// because this is only about one user. Probably rename this in APIv4 anyway. + /// Returns the total amount of plays. Probably rename this in APIv4 for clarity. /// public required int MyPlaysCount { get; set; } public required int CompletionCount { get; set; } @@ -34,4 +33,4 @@ public class ApiGameLevelOwnRelationsResponse : IApiResponse PhotoCount = dataContext.Database.GetTotalPhotosInLevelByUser(level, dataContext.User) }; } -} \ No newline at end of file +} From ba3460c91940bb745f72f114b187c5028f11286b Mon Sep 17 00:00:00 2001 From: ToasterTheBrot <95178340+Toastbrot236@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:57:14 +0100 Subject: [PATCH 5/5] Fix copy-paste error Signed-off-by: ToasterTheBrot <95178340+Toastbrot236@users.noreply.github.com> --- RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs index 10f5daaf9..3588e9f3c 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/LevelApiTests.cs @@ -145,7 +145,7 @@ public async Task CantDeleteLevelIfLevelInvalid() } [Test] - public void OnlyIncludesOwnUserRelationsWhenSignedIn() + public void OnlyIncludesOwnLevelRelationsWhenSignedIn() { using TestContext context = this.GetServer(); GameUser me = context.CreateUser(); @@ -162,4 +162,4 @@ public void OnlyIncludesOwnUserRelationsWhenSignedIn() Assert.That(response?.Data, Is.Not.Null); Assert.That(response!.Data!.OwnRelations, Is.Not.Null); } -} \ No newline at end of file +}