diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index db19f290..9874595d 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -135,6 +135,13 @@ public GamePhotoSubject AddSubjectForPhoto(GamePhoto photo, int playerId, string return subject; } + public GamePhoto? GetPhotoByAnyHash(string smallHash, string mediumHash, string largeHash, string planHash) + => this.GamePhotosIncluded.FirstOrDefault(p => + p.SmallAssetHash == smallHash || + p.MediumAssetHash == mediumHash || + p.LargeAssetHash == largeHash || + p.PlanHash == planHash); + public void RemovePhoto(GamePhoto photo) { foreach (GameUser subjectUser in this.GetUsersInPhoto(photo).ToArray()) diff --git a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs index 71e877b0..24bedbac 100644 --- a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs @@ -43,6 +43,7 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa if (body.PhotoSubjects.Count > 4) { context.Logger.LogWarning(BunkumCategory.UserContent, $"Too many subjects in photo, rejecting photo upload. Uploader: {user.UserId}"); + database.AddErrorNotification("Photo upload failed", "The photo had more than 4 players", user); return BadRequest; } @@ -52,6 +53,14 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa return Unauthorized; } + GamePhoto? existingPhoto = database.GetPhotoByAnyHash(body.SmallHash, body.MediumHash, body.LargeHash, body.PlanHash); + if (existingPhoto != null) + { + // TODO: show photo names in these error notifications once we start to deserialize and store plan data + database.AddErrorNotification("Photo upload failed", "The photo already exists on the server", user); + return BadRequest; + } + List hashes = [body.LargeHash, body.MediumHash, body.SmallHash]; foreach (string hash in hashes.Distinct()) { diff --git a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs index d392a304..8774b0a1 100644 --- a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs @@ -7,6 +7,7 @@ using Refresh.Database.Models.Photos; using Refresh.Interfaces.Game.Types.Lists; using Refresh.Database.Helpers; +using System.Security.Cryptography; namespace RefreshTests.GameServer.Tests.Photos; @@ -318,7 +319,7 @@ public void CantDeleteInvalidPhoto() Assert.That(message.StatusCode, Is.EqualTo(NotFound)); } - [Test] + [Test] public void CantDeleteOthersPhoto() { using TestContext context = this.GetServer(); @@ -381,4 +382,130 @@ public void CantDeleteOthersPhoto() response = message.Content.ReadAsXML(); Assert.That(response.Items, Has.Count.EqualTo(1)); } + + [Test] + public void CannotUploadPhotoIfDuplicateImage() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + //Upload our """photo""" + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + //Upload our """plan""" + ReadOnlySpan planData = "PLNbum"u8; + string planHash = HexHelper.BytesToHexString(SHA1.HashData(planData)); + message = client.PostAsync("/lbp/upload/" + planHash, new ByteArrayContent(planData.ToArray())).Result; + + //Upload another """plan""" + ReadOnlySpan anotherPlanData = "PLNble"u8; + string anotherPlanHash = HexHelper.BytesToHexString(SHA1.HashData(anotherPlanData)); + message = client.PostAsync("/lbp/upload/" + anotherPlanHash, new ByteArrayContent(anotherPlanData.ToArray())).Result; + + SerializedPhoto photo1 = new() + { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + AuthorName = user.Username, + SmallHash = TEST_ASSET_HASH, + MediumHash = TEST_ASSET_HASH, + LargeHash = TEST_ASSET_HASH, + PlanHash = planHash, + Level = new SerializedPhotoLevel + { + LevelId = 0, + Title = "", + Type = "pod", + } + }; + + //Upload photo once + message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo1.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + //Upload photo again (different plan hash) + SerializedPhoto photo2 = new() + { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + AuthorName = user.Username, + SmallHash = TEST_ASSET_HASH, + MediumHash = TEST_ASSET_HASH, + LargeHash = TEST_ASSET_HASH, + PlanHash = anotherPlanHash, + Level = new SerializedPhotoLevel + { + LevelId = 0, + Title = "", + Type = "pod", + } + }; + message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo2.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(1)); + } + + [Test] + public void CannotUploadPhotoIfDuplicatePlan() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + //Upload our """photo""" + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + //Upload another """photo""" + ReadOnlySpan anotherTextureData = "TEX r"u8; + string anotherTextureHash = HexHelper.BytesToHexString(SHA1.HashData(anotherTextureData)); + message = client.PostAsync("/lbp/upload/" + anotherTextureHash, new ByteArrayContent(anotherTextureData.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + //Upload our """plan""" + ReadOnlySpan planData = "PLNb"u8; + string planHash = HexHelper.BytesToHexString(SHA1.HashData(planData)); + message = client.PostAsync("/lbp/upload/" + planHash, new ByteArrayContent(planData.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + SerializedPhoto photo1 = new() + { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + AuthorName = user.Username, + SmallHash = TEST_ASSET_HASH, + MediumHash = TEST_ASSET_HASH, + LargeHash = TEST_ASSET_HASH, + PlanHash = planHash, + Level = new SerializedPhotoLevel + { + LevelId = 0, + Title = "", + Type = "pod", + } + }; + + //Upload photo once + message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo1.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + //Upload photo again (different image hashes) + SerializedPhoto photo2 = new() + { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + AuthorName = user.Username, + SmallHash = anotherTextureHash, + MediumHash = anotherTextureHash, + LargeHash = anotherTextureHash, + PlanHash = planHash, + Level = new SerializedPhotoLevel + { + LevelId = 0, + Title = "", + Type = "pod", + } + }; + message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo2.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(1)); + } } \ No newline at end of file