-
Notifications
You must be signed in to change notification settings - Fork 34
Implement comment API endpoints #917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4cb9537
Upgrade comment db methods to use DatabaseList
Toastbrot236 2147ca7
Don't reject neutral comment rating requests
Toastbrot236 dc3137d
Implement comment API endpoints and minimal user/level API responses
Toastbrot236 f493b01
Add API comment tests, improve some documentation
Toastbrot236 0e36a2b
Merge branch 'main' into comment-api
jvyden 670c047
Typo fixes
Toastbrot236 77d9540
Implement, use and test ApiRatingResponse
Toastbrot236 0948401
Rename Poster to Publisher
Toastbrot236 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
Refresh.Interfaces.APIv3/Endpoints/CommentApiEndpoints.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| using AttribDoc.Attributes; | ||
| using Bunkum.Core; | ||
| using Bunkum.Core.Endpoints; | ||
| using Bunkum.Protocols.Http; | ||
| 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.Endpoints.ApiTypes; | ||
| using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Comments; | ||
| using Refresh.Interfaces.APIv3.Extensions; | ||
|
|
||
| namespace Refresh.Interfaces.APIv3.Endpoints; | ||
|
|
||
| public class CommentApiEndpoints : EndpointGroup | ||
| { | ||
| #region Profile | ||
| [ApiV3Endpoint("users/uuid/{uuid}/comments"), Authentication(false)] | ||
| [DocSummary("Gets comments posted under the specified user's profile.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] | ||
| [DocUsesPageData] | ||
| public ApiListResponse<ApiProfileCommentResponse> GetCommentsOnProfile(RequestContext context, DataContext dataContext, string uuid) | ||
| { | ||
| GameUser? profile = dataContext.Database.GetUserByUuid(uuid); | ||
| if (profile == null) return ApiNotFoundError.UserMissingError; | ||
|
|
||
| (int skip, int count) = context.GetPageData(); | ||
|
|
||
| DatabaseList<GameProfileComment> comments = dataContext.Database.GetProfileComments(profile, count, skip); | ||
| return DatabaseListExtensions.FromOldList<ApiProfileCommentResponse, GameProfileComment>(comments, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("users/uuid/{uuid}/comments", HttpMethods.Post)] | ||
| [DocSummary("Posts the given comment under the specified user's profile.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] | ||
| public ApiResponse<ApiProfileCommentResponse> PostCommentOnProfile(RequestContext context, | ||
| DataContext dataContext, string uuid, GameUser user, ApiCommentPostRequest body) | ||
| { | ||
| GameUser? profile = dataContext.Database.GetUserByUuid(uuid); | ||
| if (profile == null) return ApiNotFoundError.UserMissingError; | ||
|
|
||
| GameProfileComment comment = dataContext.Database.PostCommentToProfile(profile, user, body.Content); | ||
| return ApiProfileCommentResponse.FromOld(comment, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("profileComments/id/{id}"), Authentication(false)] | ||
| [DocSummary("Gets the profile comment specified by its ID.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| public ApiResponse<ApiProfileCommentResponse> GetProfileComment(RequestContext context, DataContext dataContext, int id) | ||
| { | ||
| GameProfileComment? comment = dataContext.Database.GetProfileCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| return ApiProfileCommentResponse.FromOld(comment, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("profileComments/id/{id}", HttpMethods.Delete)] | ||
| [DocSummary("Deletes the profile comment specified by its ID. Fails if the user is not the comment poster or the profile owner.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| [DocError(typeof(ApiValidationError), ApiValidationError.NoCommentDeletionPermissionErrorWhen)] | ||
| public ApiOkResponse DeleteProfileComment(RequestContext context, DataContext dataContext, GameUser user, int id) | ||
| { | ||
| GameProfileComment? comment = dataContext.Database.GetProfileCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| if (user != comment.Author && user != comment.Profile) return ApiValidationError.NoCommentDeletionPermissionError; | ||
|
|
||
| dataContext.Database.DeleteProfileComment(comment); | ||
| return new ApiOkResponse(); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("profileComments/id/{id}/rate/{rawRating}", HttpMethods.Post)] | ||
| [DocSummary("Rates the profile comment specified by its ID.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| [DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)] | ||
| public ApiOkResponse RateProfileComment(RequestContext context, DataContext dataContext, GameUser user, int id, | ||
| [DocSummary("The user's new rating for the comment. -1 = dislike, 0 = neutral, 1 = like.")] string rawRating) | ||
| { | ||
| GameProfileComment? comment = dataContext.Database.GetProfileCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| // rawRating is string and not sbyte or integer because passing any out of range value will make Bunkum | ||
| // set rawRating to 0 instead, which we would here wrongly take as a neutral rating instead of an invalid value. | ||
| if (!sbyte.TryParse(rawRating, out sbyte rating) || !Enum.IsDefined(typeof(RatingType), rating)) | ||
| return ApiValidationError.RatingParseError; | ||
|
|
||
| dataContext.Database.RateProfileComment(user, comment, (RatingType)rating); | ||
| return new ApiOkResponse(); | ||
| } | ||
| #endregion | ||
|
|
||
| #region Level | ||
| [ApiV3Endpoint("levels/id/{id}/comments"), Authentication(false)] | ||
| [DocSummary("Gets comments posted under the specified level.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] | ||
| [DocUsesPageData] | ||
| public ApiListResponse<ApiLevelCommentResponse> GetCommentsOnLevel(RequestContext context, DataContext dataContext, int id) | ||
| { | ||
| GameLevel? level = dataContext.Database.GetLevelById(id); | ||
| if (level == null) return ApiNotFoundError.LevelMissingError; | ||
|
|
||
| (int skip, int count) = context.GetPageData(); | ||
|
|
||
| DatabaseList<GameLevelComment> comments = dataContext.Database.GetLevelComments(level, count, skip); | ||
| return DatabaseListExtensions.FromOldList<ApiLevelCommentResponse, GameLevelComment>(comments, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("levels/id/{id}/comments", HttpMethods.Post)] | ||
| [DocSummary("Posts the given comment under the specified level.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] | ||
| public ApiResponse<ApiLevelCommentResponse> PostCommentOnLevel(RequestContext context, | ||
| DataContext dataContext, int id, GameUser user, ApiCommentPostRequest body) | ||
| { | ||
| GameLevel? level = dataContext.Database.GetLevelById(id); | ||
| if (level == null) return ApiNotFoundError.LevelMissingError; | ||
|
|
||
| GameLevelComment comment = dataContext.Database.PostCommentToLevel(level, user, body.Content); | ||
| return ApiLevelCommentResponse.FromOld(comment, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("levelComments/id/{id}"), Authentication(false)] | ||
| [DocSummary("Gets the level comment specified by its ID.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| public ApiResponse<ApiLevelCommentResponse> GetLevelComment(RequestContext context, DataContext dataContext, int id) | ||
| { | ||
| GameLevelComment? comment = dataContext.Database.GetLevelCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| return ApiLevelCommentResponse.FromOld(comment, dataContext); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("levelComments/id/{id}", HttpMethods.Delete)] | ||
| [DocSummary("Deletes the level comment specified by its ID. Fails if the user is not the comment poster or the level publisher.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| [DocError(typeof(ApiValidationError), ApiValidationError.NoCommentDeletionPermissionErrorWhen)] | ||
| public ApiOkResponse DeleteLevelComment(RequestContext context, DataContext dataContext, GameUser user, int id) | ||
| { | ||
| GameLevelComment? comment = dataContext.Database.GetLevelCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| if (user != comment.Author && user != comment.Level.Publisher) return ApiValidationError.NoCommentDeletionPermissionError; | ||
|
|
||
| dataContext.Database.DeleteLevelComment(comment); | ||
| return new ApiOkResponse(); | ||
| } | ||
|
|
||
| [ApiV3Endpoint("levelComments/id/{id}/rate/{rawRating}", HttpMethods.Post)] | ||
| [DocSummary("Rates the level comment specified by its ID.")] | ||
| [DocError(typeof(ApiNotFoundError), ApiNotFoundError.CommentMissingErrorWhen)] | ||
| [DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)] | ||
| public ApiOkResponse RateLevelComment(RequestContext context, DataContext dataContext, GameUser user, int id, | ||
| [DocSummary("The user's new rating for the comment. -1 = dislike, 0 = neutral, 1 = like.")] string rawRating) | ||
| { | ||
| GameLevelComment? comment = dataContext.Database.GetLevelCommentById(id); | ||
| if (comment == null) return ApiNotFoundError.CommentMissingError; | ||
|
|
||
| // rawRating is string and not sbyte or integer because passing any out of range value will make Bunkum | ||
| // set rawRating to 0 instead, which we would here wrongly take as a neutral rating instead of an invalid value. | ||
| if (!sbyte.TryParse(rawRating, out sbyte rating) || !Enum.IsDefined(typeof(RatingType), rating)) | ||
| return ApiValidationError.RatingParseError; | ||
|
|
||
| dataContext.Database.RateLevelComment(user, comment, (RatingType)rating); | ||
| return new ApiOkResponse(); | ||
| } | ||
| #endregion | ||
| } | ||
7 changes: 7 additions & 0 deletions
7
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiCommentPostRequest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request; | ||
|
|
||
| [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] | ||
| public class ApiCommentPostRequest | ||
| { | ||
| public required string Content { get; set; } | ||
| } |
41 changes: 41 additions & 0 deletions
41
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Comments/ApiLevelCommentResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| using Refresh.Core.Types.Data; | ||
| using Refresh.Database.Models.Comments; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Data; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; | ||
|
|
||
| namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Comments; | ||
|
|
||
| [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] | ||
| public class ApiLevelCommentResponse : IApiResponse, IDataConvertableFrom<ApiLevelCommentResponse, GameLevelComment> | ||
| { | ||
| public required int CommentId { get; set; } | ||
| public required string Content { get; set; } | ||
| public required ApiMinimalUserResponse Publisher { get; set; } | ||
| public required ApiMinimalLevelResponse Level { get; set; } | ||
| public required ApiRatingResponse Rating { get; set; } | ||
| public required DateTimeOffset Timestamp { get; set; } | ||
|
|
||
| public static ApiLevelCommentResponse? FromOld(GameLevelComment? old, DataContext dataContext) | ||
| { | ||
| if (old == null) return null; | ||
|
|
||
| return new ApiLevelCommentResponse | ||
| { | ||
| CommentId = old.SequentialId, | ||
| Content = old.Content, | ||
| Publisher = ApiMinimalUserResponse.FromOld(old.Author, dataContext)!, | ||
| Level = ApiMinimalLevelResponse.FromOld(old.Level, dataContext)!, | ||
| Rating = ApiRatingResponse.FromRating | ||
| ( | ||
| dataContext.Database.GetTotalRatingsForLevelComment(old, RatingType.Yay), | ||
| dataContext.Database.GetTotalRatingsForLevelComment(old, RatingType.Boo), | ||
| dataContext.User != null ? (int?)dataContext.Database.GetLevelCommentRatingByUser(old, dataContext.User) : 0 | ||
| ), | ||
| Timestamp = old.Timestamp, | ||
| }; | ||
| } | ||
|
|
||
| public static IEnumerable<ApiLevelCommentResponse> FromOldList(IEnumerable<GameLevelComment> oldList, DataContext dataContext) | ||
| => oldList.Select(old => FromOld(old, dataContext)).ToList()!; | ||
| } |
40 changes: 40 additions & 0 deletions
40
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Comments/ApiProfileCommentResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| using Refresh.Core.Types.Data; | ||
| using Refresh.Database.Models.Comments; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Data; | ||
| using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; | ||
|
|
||
| namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Comments; | ||
|
|
||
| [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] | ||
| public class ApiProfileCommentResponse : IApiResponse, IDataConvertableFrom<ApiProfileCommentResponse, GameProfileComment> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto concerns for this class as well |
||
| { | ||
| public required int CommentId { get; set; } | ||
| public required string Content { get; set; } | ||
| public required ApiMinimalUserResponse Publisher { get; set; } | ||
| public required ApiMinimalUserResponse Profile { get; set; } | ||
| public required ApiRatingResponse Rating { get; set; } | ||
| public required DateTimeOffset Timestamp { get; set; } | ||
|
|
||
| public static ApiProfileCommentResponse? FromOld(GameProfileComment? old, DataContext dataContext) | ||
| { | ||
| if (old == null) return null; | ||
|
|
||
| return new ApiProfileCommentResponse | ||
| { | ||
| CommentId = old.SequentialId, | ||
| Content = old.Content, | ||
| Publisher = ApiMinimalUserResponse.FromOld(old.Author, dataContext)!, | ||
| Profile = ApiMinimalUserResponse.FromOld(old.Profile, dataContext)!, | ||
| Rating = ApiRatingResponse.FromRating | ||
| ( | ||
| dataContext.Database.GetTotalRatingsForProfileComment(old, RatingType.Yay), | ||
| dataContext.Database.GetTotalRatingsForProfileComment(old, RatingType.Boo), | ||
| dataContext.User != null ? (int?)dataContext.Database.GetProfileCommentRatingByUser(old, dataContext.User) : 0 | ||
| ), | ||
| Timestamp = old.Timestamp, | ||
| }; | ||
| } | ||
|
|
||
| public static IEnumerable<ApiProfileCommentResponse> FromOldList(IEnumerable<GameProfileComment> oldList, DataContext dataContext) | ||
| => oldList.Select(old => FromOld(old, dataContext)).ToList()!; | ||
| } | ||
19 changes: 19 additions & 0 deletions
19
Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Data/ApiRatingResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Data; | ||
|
|
||
| [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] | ||
| public class ApiRatingResponse : IApiResponse | ||
| { | ||
| public required int YayRatings { get; set; } | ||
| public required int BooRatings { get; set; } | ||
| public required int OwnRating { get; set; } | ||
|
|
||
| public static ApiRatingResponse FromRating(int yayRatings, int booRatings, int? ownRating) | ||
| { | ||
| return new() | ||
| { | ||
| YayRatings = yayRatings, | ||
| BooRatings = booRatings, | ||
| OwnRating = ownRating ?? 0, | ||
| }; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.