Refactor serialization system (#702)

* Initial work for serialization refactor

* Experiment with new naming conventions

* Mostly implement user and slot serialization.
Still needs to be fine tuned to match original implementation
Many things are left in a broken state like website features/api endpoints/lbp3 categories

* Fix release building

* Migrate scores, reviews, and more to new serialization system.
Many things are still broken but progress is steadily being made

* Fix Api responses and migrate serialization for most types

* Make serialization better and fix bugs
Fix recursive PrepareSerialization when recursive item is set during root item's PrepareSerialization, items, should be properly indexed in order but it's only tested to 1 level of recursion

* Fix review serialization

* Fix user serialization producing malformed SQL query

* Remove DefaultIfEmpty query

* MariaDB doesn't like double nested queries

* Fix LBP1 tag counter

* Implement lbp3 categories and add better deserialization handling

* Implement expression tree caching to speed up reflection and write new serializer tests

* Remove Game column from UserEntity and rename DatabaseContextModelSnapshot.cs back to DatabaseModelSnapshot.cs

* Make UserEntity username not required

* Fix recursive serialization of lists and add relevant unit tests

* Actually commit the migration

* Fix LocationTests to use new deserialization class

* Fix comments not serializing the right author username

* Replace all occurrences of StatusCode with their respective ASP.NET named result
instead of StatusCode(403) everything is now in the form of Forbid()

* Fix SlotBase.ConvertToEntity and LocationTests

* Fix compilation error

* Give Location a default value in GameUserSlot and GameUser

* Reimplement stubbed website functions

* Convert grief reports to new serialization system

* Update DatabaseModelSnapshot and bump dotnet tool version

* Remove unused directives

* Fix broken type reference

* Fix rated comments on website

* Don't include banned users in website comments

* Optimize score submission

* Fix slot id calculating in in-game comment posting

* Move serialization interfaces to types folder and add more documentation

* Allow uploading of versus scores
This commit is contained in:
Josh 2023-03-27 19:39:54 -05:00 committed by GitHub
parent 307b2135a3
commit 329ab66043
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
248 changed files with 4993 additions and 2896 deletions

View file

@ -6,12 +6,12 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -34,17 +34,17 @@ public class PhotosController : ControllerBase
[HttpPost("uploadPhoto")]
public async Task<IActionResult> UploadPhoto()
{
User? user = await this.database.UserFromGameToken(this.GetToken());
if (user == null) return this.StatusCode(403, "");
UserEntity? user = await this.database.UserFromGameToken(this.GetToken());
if (user == null) return this.Forbid();
if (user.PhotosByMe >= ServerConfiguration.Instance.UserGeneratedContentLimits.PhotosQuota) return this.BadRequest();
if (user.GetUploadedPhotoCount(this.database) >= ServerConfiguration.Instance.UserGeneratedContentLimits.PhotosQuota) return this.BadRequest();
Photo? photo = await this.DeserializeBody<Photo>();
GamePhoto? photo = await this.DeserializeBody<GamePhoto>();
if (photo == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(photo);
foreach (Photo p in this.database.Photos.Where(p => p.CreatorId == user.UserId))
foreach (PhotoEntity p in this.database.Photos.Where(p => p.CreatorId == user.UserId))
{
if (p.LargeHash == photo.LargeHash) return this.Ok(); // photo already uplaoded
if (p.MediumHash == photo.MediumHash) return this.Ok();
@ -52,20 +52,28 @@ public class PhotosController : ControllerBase
if (p.PlanHash == photo.PlanHash) return this.Ok();
}
photo.CreatorId = user.UserId;
photo.Creator = user;
PhotoEntity photoEntity = new()
{
CreatorId = user.UserId,
Creator = user,
SmallHash = photo.SmallHash,
MediumHash = photo.MediumHash,
LargeHash = photo.LargeHash,
PlanHash = photo.PlanHash,
Timestamp = photo.Timestamp,
};
if (photo.XmlLevelInfo?.RootLevel != null)
if (photo.LevelInfo?.RootLevel != null)
{
bool validLevel = false;
PhotoSlot photoSlot = photo.XmlLevelInfo;
PhotoSlot photoSlot = photo.LevelInfo;
if (photoSlot.SlotType is SlotType.Pod or SlotType.Local) photoSlot.SlotId = 0;
switch (photoSlot.SlotType)
{
case SlotType.User:
{
// We'll grab the slot by the RootLevel and see what happens from here.
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.ResourceCollection.Contains(photoSlot.RootLevel));
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.ResourceCollection.Contains(photoSlot.RootLevel));
if (slot == null) break;
if (!string.IsNullOrEmpty(slot.RootLevel)) validLevel = true;
@ -76,7 +84,7 @@ public class PhotosController : ControllerBase
case SlotType.Local:
case SlotType.Developer:
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId);
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId);
if (slot != null)
photoSlot.SlotId = slot.SlotId;
else
@ -92,23 +100,23 @@ public class PhotosController : ControllerBase
break;
}
if (validLevel) photo.SlotId = photo.XmlLevelInfo.SlotId;
if (validLevel) photoEntity.SlotId = photoSlot.SlotId;
}
if (photo.XmlSubjects?.Count > 4) return this.BadRequest();
if (photo.Subjects?.Count > 4) return this.BadRequest();
if (photo.Timestamp > TimeHelper.Timestamp) photo.Timestamp = TimeHelper.Timestamp;
this.database.Photos.Add(photo);
this.database.Photos.Add(photoEntity);
// Save to get photo ID for the PhotoSubject foreign keys
await this.database.SaveChangesAsync();
if (photo.XmlSubjects != null)
if (photo.Subjects != null)
{
// Check for duplicate photo subjects
List<string> subjectUserIds = new(4);
foreach (PhotoSubject subject in photo.PhotoSubjects)
foreach (GamePhotoSubject subject in photo.Subjects)
{
if (subjectUserIds.Contains(subject.Username) && !string.IsNullOrEmpty(subject.Username))
return this.BadRequest();
@ -116,17 +124,23 @@ public class PhotosController : ControllerBase
subjectUserIds.Add(subject.Username);
}
foreach (PhotoSubject subject in photo.XmlSubjects.Where(subject => !string.IsNullOrEmpty(subject.Username)))
foreach (GamePhotoSubject subject in photo.Subjects.Where(subject => !string.IsNullOrEmpty(subject.Username)))
{
subject.User = await this.database.Users.FirstOrDefaultAsync(u => u.Username == subject.Username);
subject.UserId = await this.database.Users.Where(u => u.Username == subject.Username)
.Select(u => u.UserId)
.FirstOrDefaultAsync();
if (subject.User == null) continue;
if (subject.UserId == 0) continue;
PhotoSubjectEntity subjectEntity = new()
{
PhotoId = photoEntity.PhotoId,
UserId = subject.UserId,
};
subject.UserId = subject.User.UserId;
subject.PhotoId = photo.PhotoId;
Logger.Debug($"Adding PhotoSubject (userid {subject.UserId}) to db", LogArea.Photos);
this.database.PhotoSubjects.Add(subject);
this.database.PhotoSubjects.Add(subjectEntity);
}
}
@ -155,16 +169,15 @@ public class PhotosController : ControllerBase
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
List<Photo> photos = await this.database.Photos.Include(p => p.Creator)
.Include(p => p.PhotoSubjects)
.ThenInclude(ps => ps.User)
List<GamePhoto> photos = await this.database.Photos.Include(p => p.PhotoSubjects)
.Where(p => p.SlotId == id)
.OrderByDescending(s => s.Timestamp)
.Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 30))
.Select(p => GamePhoto.CreateFromEntity(p))
.ToListAsync();
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(id, SlotHelper.ParseType(slotType)));
return this.Ok(LbpSerializer.StringElement("photos", response));
return this.Ok(new PhotoListResponse(photos));
}
[HttpGet("photos/by")]
@ -175,16 +188,14 @@ public class PhotosController : ControllerBase
int targetUserId = await this.database.UserIdFromUsername(user);
if (targetUserId == 0) return this.NotFound();
List<Photo> photos = await this.database.Photos.Include(p => p.Creator)
.Include(p => p.PhotoSubjects)
.ThenInclude(ps => ps.User)
List<GamePhoto> photos = await this.database.Photos.Include(p => p.PhotoSubjects)
.Where(p => p.CreatorId == targetUserId)
.OrderByDescending(s => s.Timestamp)
.Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 30))
.Select(p => GamePhoto.CreateFromEntity(p))
.ToListAsync();
string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize());
return this.Ok(LbpSerializer.StringElement("photos", response));
return this.Ok(new PhotoListResponse(photos));
}
[HttpGet("photos/with")]
@ -195,32 +206,30 @@ public class PhotosController : ControllerBase
int targetUserId = await this.database.UserIdFromUsername(user);
if (targetUserId == 0) return this.NotFound();
List<Photo> photos = await this.database.Photos.Include(p => p.Creator)
.Include(p => p.PhotoSubjects)
.ThenInclude(ps => ps.User)
List<GamePhoto> photos = await this.database.Photos.Include(p => p.PhotoSubjects)
.Where(p => p.PhotoSubjects.Any(ps => ps.UserId == targetUserId))
.OrderByDescending(s => s.Timestamp)
.Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 30))
.Select(p => GamePhoto.CreateFromEntity(p))
.ToListAsync();
string response = photos.Aggregate(string.Empty, (current, photo) => current + photo.Serialize());
return this.Ok(LbpSerializer.StringElement("photos", response));
return this.Ok(new PhotoListResponse(photos));
}
[HttpPost("deletePhoto/{id:int}")]
public async Task<IActionResult> DeletePhoto(int id)
{
GameToken token = this.GetToken();
GameTokenEntity token = this.GetToken();
Photo? photo = await this.database.Photos.FirstOrDefaultAsync(p => p.PhotoId == id);
PhotoEntity? photo = await this.database.Photos.FirstOrDefaultAsync(p => p.PhotoId == id);
if (photo == null) return this.NotFound();
// If user isn't photo creator then check if they own the level
if (photo.CreatorId != token.UserId)
{
Slot? photoSlot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == photo.SlotId && s.Type == SlotType.User);
if (photoSlot == null || photoSlot.CreatorId != token.UserId) return this.StatusCode(401, "");
SlotEntity? photoSlot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == photo.SlotId && s.Type == SlotType.User);
if (photoSlot == null || photoSlot.CreatorId != token.UserId) return this.Unauthorized();
}
HashSet<string> photoResources = new(){photo.LargeHash, photo.SmallHash, photo.MediumHash, photo.PlanHash,};