diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs index dbad276a..d357d51e 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs @@ -69,6 +69,7 @@ public class ApiExtendedGameUserResponse : ApiGameUserResponse, IApiResponse, ID UnescapeXmlSequences = user.UnescapeXmlSequences, FilesizeQuotaUsage = user.FilesizeQuotaUsage, Statistics = ApiGameUserStatisticsResponse.FromOld(user, dataContext)!, + OwnRelations = ApiGameUserOwnRelationsResponse.FromOld(user, dataContext), ActiveRoom = ApiGameRoomResponse.FromOld(dataContext.Match.RoomAccessor.GetRoomByUser(user), dataContext), LevelVisibility = user.LevelVisibility, ProfileVisibility = user.ProfileVisibility, diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserOwnRelationsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserOwnRelationsResponse.cs new file mode 100644 index 00000000..5014ab1f --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserOwnRelationsResponse.cs @@ -0,0 +1,21 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiGameUserOwnRelationsResponse : IApiResponse +{ + public required bool IsHearted { get; set; } + + public static ApiGameUserOwnRelationsResponse? FromOld(GameUser user, DataContext dataContext) + { + if (dataContext.User == null) + return null; + + return new() + { + IsHearted = dataContext.Database.IsUserFavouritedByUser(user, dataContext.User), + }; + } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserResponse.cs index 91c2f32a..f0f0a61d 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserResponse.cs @@ -26,6 +26,7 @@ public class ApiGameUserResponse : IApiResponse, IDataConvertableFrom null; notnull => notnull")] @@ -51,6 +52,7 @@ public class ApiGameUserResponse : IApiResponse, IDataConvertableFrom GetUserByUuid(RequestContext context, Ga return ApiGameUserResponse.FromOld(user, dataContext); } + + // TODO: Also allow specifying user by username + [ApiV3Endpoint("users/uuid/{uuid}/heart", HttpMethods.Post)] + [DocSummary("Hearts a user by their UUID")] + [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] + public ApiOkResponse HeartUserByUuid(RequestContext context, GameDatabaseContext database, + [DocSummary("The UUID of the user")] string uuid, DataContext dataContext, GameUser user) + { + GameUser? target = database.GetUserByUuid(uuid); + if(target == null) return ApiNotFoundError.UserMissingError; + + bool success = database.FavouriteUser(target, user); + + // Only give pin if the user was hearted without having already been hearted. + // Won't protect against spam, but this way the pin objective is more accurately implemented. + if (success) + database.IncrementUserPinProgress((long)ServerPins.HeartPlayerOnWebsite, 1, user, false, TokenPlatform.Website); + + return new ApiOkResponse(); + } + + [ApiV3Endpoint("users/uuid/{uuid}/unheart", HttpMethods.Post)] + [DocSummary("Unhearts a user by their UUID")] + [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] + public ApiOkResponse UnheartUserByUuid(RequestContext context, GameDatabaseContext database, + [DocSummary("The UUID of the user")] string uuid, DataContext dataContext, GameUser user) + { + GameUser? target = database.GetUserByUuid(uuid); + if(target == null) return ApiNotFoundError.UserMissingError; + + database.UnfavouriteUser(target, user); + return new ApiOkResponse(); + } [ApiV3Endpoint("users/me"), MinimumRole(GameUserRole.Restricted)] [DocSummary("Returns your own user, provided you are authenticated")] diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index c3f48e03..154e8118 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -343,4 +343,23 @@ public void GetsNewestUsers(bool showOnlineUsers) index++; } } + + [Test] + public void OnlyIncludesOwnUserRelationsWhenSignedIn() + { + using TestContext context = this.GetServer(); + GameUser otherUser = context.CreateUser(); + GameUser me = context.CreateUser(); + + // Try fetching the user without being signed in + ApiResponse? response = context.Http.GetData($"/api/v3/users/name/{otherUser.Username}"); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.OwnRelations, Is.Null); + + // Sign in and then get user again + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, me); + response = client.GetData($"/api/v3/users/name/{otherUser.Username}"); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.OwnRelations, Is.Not.Null); + } } \ No newline at end of file