Skip to content

Commit a83a2b4

Browse files
authored
Implement hearting users over the API (#1021)
Adds endpoints for hearting and unhearting users over the API. Hearting a user like this will also increment the hearting user's progress of the corresponding pin, meaning that this PR will close issue #395. Also adds an `OwnRelations` attribute to `ApiGameUserResponse`, which currently only contains whether the user in question is hearted.
2 parents c1b3516 + 2fd48c7 commit a83a2b4

6 files changed

Lines changed: 83 additions & 3 deletions

File tree

Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class ApiExtendedGameUserResponse : ApiGameUserResponse, IApiResponse, ID
6969
UnescapeXmlSequences = user.UnescapeXmlSequences,
7070
FilesizeQuotaUsage = user.FilesizeQuotaUsage,
7171
Statistics = ApiGameUserStatisticsResponse.FromOld(user, dataContext)!,
72+
OwnRelations = ApiGameUserOwnRelationsResponse.FromOld(user, dataContext),
7273
ActiveRoom = ApiGameRoomResponse.FromOld(dataContext.Match.RoomAccessor.GetRoomByUser(user), dataContext),
7374
LevelVisibility = user.LevelVisibility,
7475
ProfileVisibility = user.ProfileVisibility,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Refresh.Core.Types.Data;
2+
using Refresh.Database.Models.Users;
3+
4+
namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users;
5+
6+
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
7+
public class ApiGameUserOwnRelationsResponse : IApiResponse
8+
{
9+
public required bool IsHearted { get; set; }
10+
11+
public static ApiGameUserOwnRelationsResponse? FromOld(GameUser user, DataContext dataContext)
12+
{
13+
if (dataContext.User == null)
14+
return null;
15+
16+
return new()
17+
{
18+
IsHearted = dataContext.Database.IsUserFavouritedByUser(user, dataContext.User),
19+
};
20+
}
21+
}

Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiGameUserResponse.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class ApiGameUserResponse : IApiResponse, IDataConvertableFrom<ApiGameUse
2626
public required GameUserRole Role { get; set; }
2727

2828
public required ApiGameUserStatisticsResponse Statistics { get; set; }
29+
public required ApiGameUserOwnRelationsResponse? OwnRelations { get; set; }
2930
public required ApiGameRoomResponse? ActiveRoom { get; set; }
3031

3132
[ContractAnnotation("null => null; notnull => notnull")]
@@ -51,6 +52,7 @@ public class ApiGameUserResponse : IApiResponse, IDataConvertableFrom<ApiGameUse
5152
LastLoginDate = user.LastLoginDate,
5253
Role = user.Role,
5354
Statistics = ApiGameUserStatisticsResponse.FromOld(user, dataContext)!,
55+
OwnRelations = ApiGameUserOwnRelationsResponse.FromOld(user, dataContext),
5456
ActiveRoom = ApiGameRoomResponse.FromOld(dataContext.Match.RoomAccessor.GetRoomByUser(user), dataContext),
5557
};
5658
}

Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,12 @@ public ApiOkResponse QueueLevel(RequestContext context, GameDatabaseContext data
181181
GameLevel? level = database.GetLevelById(id);
182182
if (level == null) return ApiNotFoundError.LevelMissingError;
183183

184-
database.QueueLevel(level, user);
184+
bool success = database.QueueLevel(level, user);
185185

186-
// Update pin progress for queueing a level through the API
187-
database.IncrementUserPinProgress((long)ServerPins.QueueLevelOnWebsite, 1, user, false, TokenPlatform.Website);
186+
// Only give pin if the level was queued without having already been queued.
187+
// Won't protect against spam, but this way the pin objective is more accurately implemented.
188+
if (success)
189+
database.IncrementUserPinProgress((long)ServerPins.QueueLevelOnWebsite, 1, user, false, TokenPlatform.Website);
188190

189191
return new ApiOkResponse();
190192
}

Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using Refresh.Core.Services;
1010
using Refresh.Core.Types.Data;
1111
using Refresh.Database;
12+
using Refresh.Database.Models.Authentication;
13+
using Refresh.Database.Models.Pins;
1214
using Refresh.Database.Models.Users;
1315
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
1416
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
@@ -45,6 +47,39 @@ public ApiResponse<ApiGameUserResponse> GetUserByUuid(RequestContext context, Ga
4547

4648
return ApiGameUserResponse.FromOld(user, dataContext);
4749
}
50+
51+
// TODO: Also allow specifying user by username
52+
[ApiV3Endpoint("users/uuid/{uuid}/heart", HttpMethods.Post)]
53+
[DocSummary("Hearts a user by their UUID")]
54+
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
55+
public ApiOkResponse HeartUserByUuid(RequestContext context, GameDatabaseContext database,
56+
[DocSummary("The UUID of the user")] string uuid, DataContext dataContext, GameUser user)
57+
{
58+
GameUser? target = database.GetUserByUuid(uuid);
59+
if(target == null) return ApiNotFoundError.UserMissingError;
60+
61+
bool success = database.FavouriteUser(target, user);
62+
63+
// Only give pin if the user was hearted without having already been hearted.
64+
// Won't protect against spam, but this way the pin objective is more accurately implemented.
65+
if (success)
66+
database.IncrementUserPinProgress((long)ServerPins.HeartPlayerOnWebsite, 1, user, false, TokenPlatform.Website);
67+
68+
return new ApiOkResponse();
69+
}
70+
71+
[ApiV3Endpoint("users/uuid/{uuid}/unheart", HttpMethods.Post)]
72+
[DocSummary("Unhearts a user by their UUID")]
73+
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
74+
public ApiOkResponse UnheartUserByUuid(RequestContext context, GameDatabaseContext database,
75+
[DocSummary("The UUID of the user")] string uuid, DataContext dataContext, GameUser user)
76+
{
77+
GameUser? target = database.GetUserByUuid(uuid);
78+
if(target == null) return ApiNotFoundError.UserMissingError;
79+
80+
database.UnfavouriteUser(target, user);
81+
return new ApiOkResponse();
82+
}
4883

4984
[ApiV3Endpoint("users/me"), MinimumRole(GameUserRole.Restricted)]
5085
[DocSummary("Returns your own user, provided you are authenticated")]

RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,4 +343,23 @@ public void GetsNewestUsers(bool showOnlineUsers)
343343
index++;
344344
}
345345
}
346+
347+
[Test]
348+
public void OnlyIncludesOwnUserRelationsWhenSignedIn()
349+
{
350+
using TestContext context = this.GetServer();
351+
GameUser otherUser = context.CreateUser();
352+
GameUser me = context.CreateUser();
353+
354+
// Try fetching the user without being signed in
355+
ApiResponse<ApiGameUserResponse>? response = context.Http.GetData<ApiGameUserResponse>($"/api/v3/users/name/{otherUser.Username}");
356+
Assert.That(response?.Data, Is.Not.Null);
357+
Assert.That(response!.Data!.OwnRelations, Is.Null);
358+
359+
// Sign in and then get user again
360+
using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, me);
361+
response = client.GetData<ApiGameUserResponse>($"/api/v3/users/name/{otherUser.Username}");
362+
Assert.That(response?.Data, Is.Not.Null);
363+
Assert.That(response!.Data!.OwnRelations, Is.Not.Null);
364+
}
346365
}

0 commit comments

Comments
 (0)