forked from LittleBigRefresh/Refresh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReviewApiEndpoints.cs
More file actions
238 lines (201 loc) · 11.7 KB
/
Copy pathReviewApiEndpoints.cs
File metadata and controls
238 lines (201 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
using AttribDoc.Attributes;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.RateLimit;
using Bunkum.Core.Storage;
using Bunkum.Protocols.Http;
using Refresh.Common.Constants;
using Refresh.Core.Configuration;
using Refresh.Core.RateLimits.Relations;
using Refresh.Core.RateLimits.Reviews;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Comments;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Documentation.Attributes;
using Refresh.Interfaces.APIv3.Documentation.Descriptions;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels;
using Refresh.Interfaces.APIv3.Extensions;
namespace Refresh.Interfaces.APIv3.Endpoints;
public class ReviewApiEndpoints : EndpointGroup
{
[ApiV3Endpoint("levels/id/{id}/reviews"), Authentication(false)]
[DocUsesPageData, DocSummary("Gets a list of the reviews posted to a level.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
[RateLimitSettings(ReviewListEndpointLimits.TimeoutDuration, ReviewListEndpointLimits.RequestAmount,
ReviewListEndpointLimits.BlockDuration, ReviewListEndpointLimits.RequestBucket)]
public ApiListResponse<ApiGameReviewResponse> GetReviewsForLevel(RequestContext context,
GameDatabaseContext database, IDataStore dataStore,
[DocSummary("The ID of the level")] int id, DataContext dataContext)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;
(int skip, int count) = context.GetPageData();
DatabaseList<GameReview> reviews = database.GetReviewsForLevel(level, count, skip);
DatabaseList<ApiGameReviewResponse> ret = DatabaseListExtensions.FromOldList<ApiGameReviewResponse, GameReview>(reviews, dataContext);
return ret;
}
[ApiV3Endpoint("users/{idType}/{id}/reviews"), Authentication(false)]
[DocUsesPageData, DocSummary("Gets a list of the reviews posted by a user.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
[RateLimitSettings(ReviewListEndpointLimits.TimeoutDuration, ReviewListEndpointLimits.RequestAmount,
ReviewListEndpointLimits.BlockDuration, ReviewListEndpointLimits.RequestBucket)]
public ApiListResponse<ApiGameReviewResponse> GetReviewsByUser(RequestContext context,
GameDatabaseContext database, DataContext dataContext,
[DocSummary(SharedParamDescriptions.UserIdParam)] string id,
[DocSummary(SharedParamDescriptions.UserIdTypeParam)] string idType)
{
GameUser? user = database.GetUserByIdAndType(idType, id);
if (user == null) return ApiNotFoundError.UserMissingError;
(int skip, int count) = context.GetPageData();
DatabaseList<GameReview> reviews = database.GetReviewsByUser(user, count, skip);
DatabaseList<ApiGameReviewResponse> ret = DatabaseListExtensions.FromOldList<ApiGameReviewResponse, GameReview>(reviews, dataContext);
return ret;
}
[ApiV3Endpoint("reviews/id/{id}"), Authentication(false)]
[DocSummary("Gets a review by ID.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ReviewMissingErrorWhen)]
[RateLimitSettings(SingleReviewEndpointLimits.TimeoutDuration, SingleReviewEndpointLimits.RequestAmount,
SingleReviewEndpointLimits.BlockDuration, SingleReviewEndpointLimits.RequestBucket)]
public ApiResponse<ApiGameReviewResponse> GetReviewById(RequestContext context,
GameDatabaseContext database, IDataStore dataStore,
[DocSummary("The ID of the review")] int id, DataContext dataContext)
{
GameReview? review = database.GetReviewById(id);
if (review == null) return ApiNotFoundError.ReviewMissingError;
return ApiGameReviewResponse.FromOld(review, dataContext);
}
/// <returns>
/// The validated list of labels. Labels which are duplicates or invalid will be removed.
/// Also, the list will be trimmed if there are too many labels.
/// </returns>
private List<Label> ValidateLabels(List<Label> input)
=> input
.Distinct()
.Where(l => Enum.IsDefined(l))
.Take(UgcLimits.MaximumLabels)
.ToList();
[ApiV3Endpoint("levels/id/{id}/reviews", HttpMethods.Post)]
[DocSummary("Posts a review to the specified level. Updates the user's current review if they've already posted one.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.DontReviewOwnLevelWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.DontReviewLevelBeforePlayingWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)]
[RateLimitSettings(ReviewUploadEndpointLimits.TimeoutDuration, ReviewUploadEndpointLimits.RequestAmount,
ReviewUploadEndpointLimits.BlockDuration, ReviewUploadEndpointLimits.RequestBucket)]
public ApiResponse<ApiGameReviewResponse> PostReviewToLevel(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, GameUser user,
[DocSummary("The ID of the level")] int id, ApiSubmitReviewRequest body, DataContext dataContext, GameServerConfig config)
{
if (user.IsWriteBlocked(config))
return ApiAuthenticationError.ReadOnlyError;
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;
// Level publishers shouldn't be able to review their own level
if (user.UserId == level.Publisher?.UserId)
return ApiValidationError.DontReviewOwnLevel;
// A user has to play a level before they may review it
if (!database.HasUserPlayedLevel(level, user))
return ApiValidationError.DontReviewLevelBeforePlaying;
// Validate labels
if (body.Labels != null)
{
body.Labels = this.ValidateLabels(body.Labels);
}
if (body.LevelRating != null)
{
if (!Enum.IsDefined(typeof(RatingType), body.LevelRating))
return ApiValidationError.RatingParseError;
database.RateLevel(level, user, body.LevelRating.Value);
dataContext.Cache.UpdateLevelRatingByUser(user, level, (int)body.LevelRating.Value, database);
}
if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit)
{
body.Content = body.Content[..UgcLimits.CommentLimit];
}
GameReview review = database.AddReviewToLevel(body, level, user);
return ApiGameReviewResponse.FromOld(review, dataContext);
}
[ApiV3Endpoint("reviews/id/{id}", HttpMethods.Patch)]
[DocSummary("Updates a review by ID.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ReviewMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.NoReviewEditPermissionErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)]
[RateLimitSettings(ReviewUploadEndpointLimits.TimeoutDuration, ReviewUploadEndpointLimits.RequestAmount,
ReviewUploadEndpointLimits.BlockDuration, ReviewUploadEndpointLimits.RequestBucket)]
public ApiResponse<ApiGameReviewResponse> UpdateReviewById(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, GameUser user,
[DocSummary("The ID of the review")] int id, ApiSubmitReviewRequest body, DataContext dataContext, GameServerConfig config)
{
if (user.IsWriteBlocked(config))
return ApiAuthenticationError.ReadOnlyError;
GameReview? review = database.GetReviewById(id);
if (review == null) return ApiNotFoundError.ReviewMissingError;
// Don't allow users to update other users' reviews
if (review.PublisherUserId != user.UserId)
return ApiValidationError.NoReviewEditPermissionError;
// Validate labels
if (body.Labels != null)
{
body.Labels = this.ValidateLabels(body.Labels);
}
if (body.LevelRating != null)
{
if (!Enum.IsDefined(typeof(RatingType), body.LevelRating))
return ApiValidationError.RatingParseError;
database.RateLevel(review.Level, user, body.LevelRating.Value);
dataContext.Cache.UpdateLevelRatingByUser(user, review.Level, (int)body.LevelRating.Value, database);
}
if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit)
{
body.Content = body.Content[..UgcLimits.CommentLimit];
}
review = database.UpdateReview(body, review);
return ApiGameReviewResponse.FromOld(review, dataContext);
}
[ApiV3Endpoint("reviews/id/{id}", HttpMethods.Delete)]
[DocSummary("Deletes a review by ID.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ReviewMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.NoReviewDeletionPermissionErrorWhen)]
public ApiOkResponse DeleteReviewById(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, GameUser user,
[DocSummary("The ID of the review")] int id, DataContext dataContext)
{
GameReview? review = database.GetReviewById(id);
if (review == null) return ApiNotFoundError.ReviewMissingError;
// Only allow this review to be deleted by either its publisher or its level's publisher
if (user.UserId != review.PublisherUserId && user.UserId != review.Level.Publisher?.UserId)
return ApiValidationError.NoReviewDeletionPermissionError;
database.DeleteReview(review);
return new ApiOkResponse();
}
[ApiV3Endpoint("reviews/id/{id}/rate/{rawRating}", HttpMethods.Post)]
[DocSummary("Rates a review by ID.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ReviewMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.DontRateOwnContentWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)]
[RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount,
CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)]
public ApiOkResponse RateReviewById(RequestContext context,
GameDatabaseContext database, IDataStore dataStore, GameUser user,
[DocSummary("The ID of the review")] int id, DataContext dataContext,
[DocSummary("The user's new rating. -1 = dislike, 0 = neutral, 1 = like.")] string rawRating, GameServerConfig config)
{
if (user.IsWriteBlocked(config))
return ApiAuthenticationError.ReadOnlyError;
GameReview? review = database.GetReviewById(id);
if (review == null) return ApiNotFoundError.ReviewMissingError;
// Review publishers shouldn't be able to rate their own review
if (user.UserId == review.PublisherUserId)
return ApiValidationError.DontRateOwnContent;
// See CommentApiEndpoints.RateLevelComment()
if (!sbyte.TryParse(rawRating, out sbyte rating) || !Enum.IsDefined(typeof(RatingType), rating))
return ApiValidationError.RatingParseError;
dataContext.Database.RateReview(review, (RatingType)rating, user);
return new ApiOkResponse();
}
}