Skip to content

Commit eaebb82

Browse files
authored
Comment game endpoint improvements (#959)
This PR: - Moves the character limit enforced by the comment game endpoints to `UgcLimits`. The game endpoints now use the limit from there, but the API endpoints don't yet - Allows profile owners and level publishers to delete comments off their content from in-game aswell, to make that behaviour consistent with the API and to try and avoid confusing users. This closes #474. To make abuse of this less viable, maybe in the future we should notify a user if anyone else deletes their comments (would probably already have to be done for deletion by staff anyway), and maybe also temporarily log deleted comments for moderation - Deduplicates the `rateComment/developer/{content}` and `rateComment/user/{content}` endpoint routes into one `rateComment/{slotType}/{content}` route for consistency - Adds and adjusts some tests for the above
2 parents 2ba086d + 1fcfc86 commit eaebb82

7 files changed

Lines changed: 128 additions & 32 deletions

File tree

Refresh.Common/Constants/UgcLimits.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public static class UgcLimits
99
// String limits
1010
public const int TitleLimit = 64;
1111
public const int DescriptionLimit = 512;
12+
public const int CommentLimit = 4096;
1213
}

Refresh.Database/Models/Comments/GameLevelComment.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Refresh.Database.Models.Users;
22
using Refresh.Database.Models.Levels;
3+
using MongoDB.Bson;
34

45
namespace Refresh.Database.Models.Comments;
56

@@ -10,8 +11,8 @@ public partial class GameLevelComment : IGameComment, ISequentialId
1011
[Key] public int SequentialId { get; set; }
1112

1213
/// <inheritdoc/>
13-
[Required]
14-
public GameUser Author { get; set; } = null!;
14+
[Required, ForeignKey(nameof(AuthorUserId))] public GameUser Author { get; set; } = null!;
15+
[Required] public ObjectId AuthorUserId { get; set; }
1516

1617
/// <summary>
1718
/// The destination level this comment was posted to.

Refresh.Database/Models/Comments/GameProfileComment.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using MongoDB.Bson;
12
using Refresh.Database.Models.Users;
23

34
namespace Refresh.Database.Models.Comments;
@@ -9,8 +10,8 @@ public partial class GameProfileComment : IGameComment, ISequentialId
910
[Key] public int SequentialId { get; set; }
1011

1112
/// <inheritdoc/>
12-
[Required]
13-
public GameUser Author { get; set; } = null!;
13+
[Required, ForeignKey(nameof(AuthorUserId))] public GameUser Author { get; set; } = null!;
14+
[Required] public ObjectId AuthorUserId { get; set; }
1415

1516
/// <summary>
1617
/// The destination profile this comment was posted to.

Refresh.Database/Models/Levels/GameLevel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using MongoDB.Bson;
23
using Refresh.Database.Models.Authentication;
34
using Refresh.Database.Models.Statistics;
45
using Refresh.Database.Models.Users;
@@ -81,7 +82,9 @@ [NotMapped] public int SequentialId
8182
set => this.LevelId = value;
8283
}
8384

84-
public GameUser? Publisher { get; set; }
85+
public ObjectId? PublisherUserId { get; set; }
86+
[ForeignKey(nameof(PublisherUserId))] public GameUser? Publisher { get; set; }
87+
8588
/// <summary>
8689
/// The publisher who originally published the level, if it has been re-uploaded by someone else.
8790
/// Should only be set if the original publisher does not have an account.

Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bunkum.Core.Responses;
44
using Bunkum.Listener.Protocol;
55
using Bunkum.Protocols.Http;
6+
using Refresh.Common.Constants;
67
using Refresh.Common.Time;
78
using Refresh.Core.Authentication.Permission;
89
using Refresh.Core.Configuration;
@@ -25,13 +26,14 @@ public Response PostProfileComment(RequestContext context, GameDatabaseContext d
2526
if (user.IsWriteBlocked(config))
2627
return Unauthorized;
2728

28-
if (body.Content.Length > 4096)
29+
GameUser? profile = database.GetUserByUsername(username);
30+
if (profile == null) return NotFound;
31+
32+
if (body.Content.Length > UgcLimits.CommentLimit)
2933
{
34+
database.AddErrorNotification("Failed to post comment", $"Your comment under {profile.Username}'s profile couldn't be posted because it was too long.", user);
3035
return BadRequest;
3136
}
32-
33-
GameUser? profile = database.GetUserByUsername(username);
34-
if (profile == null) return NotFound;
3537

3638
// TODO: include a check for if the user wants to receive these types of notifications
3739
if (!profile.Equals(user))
@@ -68,8 +70,9 @@ public Response DeleteProfileComment(RequestContext context, GameDatabaseContext
6870
GameProfileComment? comment = database.GetProfileCommentById(commentId);
6971
if (comment == null) return BadRequest;
7072

71-
//Validate someone doesnt try to delete someone elses comment
72-
if (comment.Author.UserId != user.UserId)
73+
// Validate someone doesnt try to delete someone elses comment.
74+
// Also allow profile owners to delete any comment off their profile to not make the game make it look like us not implementing this is a bug.
75+
if (user.UserId != comment.AuthorUserId && user.UserId != profile.UserId)
7376
{
7477
context.Logger.LogWarning(BunkumCategory.Game, $"User {user.Username} attempted to delete someone elses comment! This is likely a forged request");
7578
return Unauthorized;
@@ -88,13 +91,14 @@ public Response PostLevelComment(RequestContext context, GameDatabaseContext dat
8891
if (user.IsWriteBlocked(config))
8992
return Unauthorized;
9093

91-
if (body.Content.Length > 4096)
94+
GameLevel? level = database.GetLevelByIdAndType(slotType, id);
95+
if (level == null) return NotFound;
96+
97+
if (body.Content.Length > UgcLimits.CommentLimit)
9298
{
99+
database.AddErrorNotification("Failed to post comment", $"Your comment under the level '{level.Title}' couldn't be posted because it was too long.", user);
93100
return BadRequest;
94101
}
95-
96-
GameLevel? level = database.GetLevelByIdAndType(slotType, id);
97-
if (level == null) return NotFound;
98102

99103
if (level.Publisher != null && !level.Publisher.Equals(user))
100104
{
@@ -131,8 +135,9 @@ public Response DeleteLevelComment(RequestContext context, GameDatabaseContext d
131135
GameLevelComment? comment = database.GetLevelCommentById(commentId);
132136
if (comment == null) return BadRequest;
133137

134-
//Validate someone doesnt try to delete someone else's comment
135-
if (comment.Author.UserId != user.UserId)
138+
// Validate someone doesnt try to delete someone else's comment.
139+
// Also allow level publishers to delete any comment off their level to not make the game make it look like us not implementing this is a bug.
140+
if (comment.AuthorUserId != user.UserId && user.UserId != level.PublisherUserId)
136141
{
137142
context.Logger.LogWarning(BunkumCategory.Game, $"User {user.Username} attempted to delete someone else's comment! This is likely a forged request");
138143
return Unauthorized;
@@ -157,13 +162,14 @@ public Response RateProfileComment(RequestContext context, GameDatabaseContext d
157162
return OK;
158163
}
159164

160-
[GameEndpoint("rateComment/user/{content}", HttpMethods.Post)] // `user` level comments
161-
[GameEndpoint("rateComment/developer/{content}", HttpMethods.Post)] // `developer` level comments
162-
public Response RateLevelComment(RequestContext context, GameDatabaseContext database, GameUser user, string content)
165+
[GameEndpoint("rateComment/{slotType}/{content}", HttpMethods.Post)]
166+
public Response RateLevelComment(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, string content)
163167
{
164168
if (!int.TryParse(context.QueryString["commentId"], out int commentId)) return BadRequest;
165169
if (!Enum.TryParse(context.QueryString["rating"], out RatingType ratingType)) return BadRequest;
166170

171+
if (slotType is not "user" and not "developer") return BadRequest;
172+
167173
GameLevelComment? comment = database.GetLevelCommentById(commentId);
168174
if (comment == null)
169175
return NotFound;

RefreshTests.GameServer/Tests/Comments/LevelCommentTests.cs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ public void CantDeleteNonExistentLevelComment()
138138
public void CantDeleteAnotherUsersComment()
139139
{
140140
using TestContext context = this.GetServer();
141-
GameUser user1 = context.CreateUser();
142-
GameUser user2 = context.CreateUser();
143-
GameLevel level = context.CreateLevel(user1);
141+
GameUser publisher = context.CreateUser();
142+
GameUser moron = context.CreateUser();
143+
GameLevel level = context.CreateLevel(publisher);
144144

145-
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, user1);
146-
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, user2);
145+
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, publisher);
146+
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, moron);
147147

148148
SerializedComment comment = new()
149149
{
@@ -163,6 +163,48 @@ public void CantDeleteAnotherUsersComment()
163163

164164
response = client2.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty<byte>())).Result;
165165
Assert.That(response.StatusCode, Is.EqualTo(Unauthorized));
166+
167+
// Make sure the comment is still there
168+
response = client1.GetAsync($"/lbp/comments/user/{level.LevelId}").Result;
169+
userComments = response.Content.ReadAsXML<SerializedCommentList>();
170+
Assert.That(userComments.Items, Has.Count.EqualTo(1));
171+
Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content));
172+
}
173+
174+
[Test]
175+
public void CanDeleteAnotherUsersCommentAsLevelPublisher()
176+
{
177+
using TestContext context = this.GetServer();
178+
GameUser publisher = context.CreateUser();
179+
GameUser author = context.CreateUser();
180+
GameLevel level = context.CreateLevel(publisher);
181+
182+
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, author);
183+
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, publisher);
184+
185+
SerializedComment comment = new()
186+
{
187+
Content = "This is a test comment!",
188+
CommentId = 0,
189+
Timestamp = 0,
190+
Handle = null,
191+
};
192+
193+
HttpResponseMessage response = client1.PostAsync($"/lbp/postComment/user/{level.LevelId}", new StringContent(comment.AsXML())).Result;
194+
Assert.That(response.StatusCode, Is.EqualTo(OK));
195+
196+
response = client1.GetAsync($"/lbp/comments/user/{level.LevelId}").Result;
197+
SerializedCommentList userComments = response.Content.ReadAsXML<SerializedCommentList>();
198+
Assert.That(userComments.Items, Has.Count.EqualTo(1));
199+
Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content));
200+
201+
response = client2.PostAsync($"/lbp/deleteComment/user/{level.LevelId}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty<byte>())).Result;
202+
Assert.That(response.StatusCode, Is.EqualTo(OK));
203+
204+
// Make sure the comment is now gone
205+
response = client1.GetAsync($"/lbp/comments/user/{level.LevelId}").Result;
206+
userComments = response.Content.ReadAsXML<SerializedCommentList>();
207+
Assert.That(userComments.Items, Has.Count.EqualTo(0));
166208
}
167209

168210
[Test]

RefreshTests.GameServer/Tests/Comments/UserCommentTests.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,12 @@ public void CantDeleteNonExistantComment()
135135
public void CantDeleteAnotherUsersComment()
136136
{
137137
using TestContext context = this.GetServer();
138-
GameUser user1 = context.CreateUser();
139-
GameUser user2 = context.CreateUser();
138+
GameUser publisher = context.CreateUser();
139+
GameUser profile = context.CreateUser();
140+
GameUser moron = context.CreateUser();
140141

141-
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, user1);
142-
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, user2);
142+
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, publisher);
143+
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, moron);
143144

144145
SerializedComment comment = new()
145146
{
@@ -149,16 +150,57 @@ public void CantDeleteAnotherUsersComment()
149150
Handle = null,
150151
};
151152

152-
HttpResponseMessage response = client1.PostAsync($"/lbp/postUserComment/{user2.Username}", new StringContent(comment.AsXML())).Result;
153+
HttpResponseMessage response = client1.PostAsync($"/lbp/postUserComment/{profile.Username}", new StringContent(comment.AsXML())).Result;
153154
Assert.That(response.StatusCode, Is.EqualTo(OK));
154155

155-
response = client1.GetAsync($"/lbp/userComments/{user2.Username}").Result;
156+
response = client1.GetAsync($"/lbp/userComments/{profile.Username}").Result;
156157
SerializedCommentList userComments = response.Content.ReadAsXML<SerializedCommentList>();
157158
Assert.That(userComments.Items, Has.Count.EqualTo(1));
158159
Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content));
159160

160-
response = client2.PostAsync($"/lbp/deleteUserComment/{user2.Username}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty<byte>())).Result;
161+
response = client2.PostAsync($"/lbp/deleteUserComment/{profile.Username}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty<byte>())).Result;
161162
Assert.That(response.StatusCode, Is.EqualTo(Unauthorized));
163+
164+
// Make sure the comment is still there
165+
response = client1.GetAsync($"/lbp/userComments/{profile.Username}").Result;
166+
userComments = response.Content.ReadAsXML<SerializedCommentList>();
167+
Assert.That(userComments.Items, Has.Count.EqualTo(1));
168+
Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content));
169+
}
170+
171+
[Test]
172+
public void CanDeleteAnotherUsersCommentAsProfileOwner()
173+
{
174+
using TestContext context = this.GetServer();
175+
GameUser publisher = context.CreateUser();
176+
GameUser profile = context.CreateUser();
177+
178+
using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, publisher);
179+
using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, profile);
180+
181+
SerializedComment comment = new()
182+
{
183+
Content = "This is a test comment!",
184+
CommentId = 0,
185+
Timestamp = 0,
186+
Handle = null,
187+
};
188+
189+
HttpResponseMessage response = client1.PostAsync($"/lbp/postUserComment/{profile.Username}", new StringContent(comment.AsXML())).Result;
190+
Assert.That(response.StatusCode, Is.EqualTo(OK));
191+
192+
response = client1.GetAsync($"/lbp/userComments/{profile.Username}").Result;
193+
SerializedCommentList userComments = response.Content.ReadAsXML<SerializedCommentList>();
194+
Assert.That(userComments.Items, Has.Count.EqualTo(1));
195+
Assert.That(userComments.Items[0].Content, Is.EqualTo(comment.Content));
196+
197+
response = client2.PostAsync($"/lbp/deleteUserComment/{profile.Username}?commentId={userComments.Items[0].CommentId}", new ByteArrayContent(Array.Empty<byte>())).Result;
198+
Assert.That(response.StatusCode, Is.EqualTo(OK));
199+
200+
// Make sure the comment is now gone
201+
response = client1.GetAsync($"/lbp/userComments/{profile.Username}").Result;
202+
userComments = response.Content.ReadAsXML<SerializedCommentList>();
203+
Assert.That(userComments.Items, Has.Count.EqualTo(0));
162204
}
163205

164206
[Test]

0 commit comments

Comments
 (0)