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
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

@ -1,4 +1,4 @@
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.User
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity
<form method="post" action="/admin/user/@Model.UserId/setGrantedSlots">
@Html.AntiForgeryToken()

View file

@ -2,6 +2,7 @@
@using System.IO
@using LBPUnion.ProjectLighthouse.Localization
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types.Entities.Interaction
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
@{
@ -42,65 +43,70 @@
<div class="ui divider"></div>
}
}
@{
int i = 0;
foreach (KeyValuePair<CommentEntity, RatedCommentEntity?> commentAndReaction in Model.Comments)
{
CommentEntity comment = commentAndReaction.Key;
int yourThumb = commentAndReaction.Value?.Rating ?? 0;
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000).ToLocalTime();
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
@for(int i = 0; i < Model.Comments.Count; i++)
{
Comment comment = Model.Comments[i];
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000).ToLocalTime();
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
string? url = Url.RouteUrl(ViewContext.RouteData.Values);
if (url == null) continue;
string decodedMessage = messageWriter.ToString();
string? url = Url.RouteUrl(ViewContext.RouteData.Values);
if (url == null) continue;
int rating = comment.ThumbsUp - comment.ThumbsDown;
int rating = comment.ThumbsUp - comment.ThumbsDown;
<div style="display: flex" id="@comment.CommentId">
@{
string style = "";
if (Model.User?.UserId == comment.PosterUserId)
{
style = "pointer-events: none";
<div style="display: flex" id="@comment.CommentId">
@{
string style = "";
if (Model.User?.UserId == comment.PosterUserId)
{
style = "pointer-events: none";
}
}
}
<div class="voting" style="@(style)">
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == 1 ? 0 : 1)">
<i class="fitted @(comment.YourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i>
</a>
<span style="text-align: center; margin: auto; @(rating < 0 ? "margin-left: -5px" : "")">@(rating)</span>
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == -1 ? 0 : -1)">
<i class="fitted @(comment.YourThumb == -1 ? "red" : "grey") arrow down link icon" style="display: block"></i>
</a>
</div>
<div class="voting" style="@(style)">
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(yourThumb == 1 ? 0 : 1)">
<i class="fitted @(yourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i>
</a>
<span style="text-align: center; margin: auto; @(rating < 0 ? "margin-left: -5px" : "")">@(rating)</span>
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(yourThumb == -1 ? 0 : -1)">
<i class="fitted @(yourThumb == -1 ? "red" : "grey") arrow down link icon" style="display: block"></i>
</a>
</div>
<div class="comment">
<b>@await comment.Poster.ToLink(Html, ViewData, language): </b>
@if (comment.Deleted)
{
<i>
<div class="comment">
<b>@await comment.Poster.ToLink(Html, ViewData, language): </b>
@if (comment.Deleted)
{
<i>
<span>@decodedMessage</span>
</i>
}
else
{
<span>@decodedMessage</span>
</i>
}
else
{
<span>@decodedMessage</span>
}
@if (((Model.User?.IsModerator ?? false) || Model.User?.UserId == comment.PosterUserId || Model.User?.UserId == pageOwnerId) && !comment.Deleted)
{
<button class="ui red icon button" style="display:inline-flex; float: right" onclick="deleteComment(@comment.CommentId)">
<i class="trash icon"></i>
</button>
}
<p>
<i>@TimeZoneInfo.ConvertTime(timestamp, timeZoneInfo).ToString("M/d/yyyy @ h:mm:ss tt")</i>
</p>
@if (i != Model.Comments.Count - 1)
{
<div class="ui divider"></div>
}
}
@if (((Model.User?.IsModerator ?? false) || Model.User?.UserId == comment.PosterUserId || Model.User?.UserId == pageOwnerId) && !comment.Deleted)
{
<button class="ui red icon button" style="display:inline-flex; float: right" onclick="deleteComment(@comment.CommentId)">
<i class="trash icon"></i>
</button>
}
<p>
<i>@TimeZoneInfo.ConvertTime(timestamp, timeZoneInfo).ToString("M/d/yyyy @ h:mm:ss tt")</i>
</p>
@if (i != Model.Comments.Count - 1)
{
<div class="ui divider"></div>
}
</div>
</div>
</div>
i++;
}
}
<script>
function deleteComment(commentId){

View file

@ -23,7 +23,7 @@
<div class="ui list">
@for(int i = 0; i < Model.Scores.Count; i++)
{
Score score = Model.Scores[i];
ScoreEntity score = Model.Scores[i];
string[] playerIds = score.PlayerIds;
DatabaseContext database = Model.Database;
<div class="item">
@ -41,7 +41,7 @@
<div class="list" style="padding-top: 0">
@for (int j = 0; j < playerIds.Length; j++)
{
User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]);
UserEntity? user = await database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]);
<div class="item">
<i class="minus icon" style="padding-top: 9px"></i>
<div class="content" style="padding-left: 0">

View file

@ -1,12 +1,14 @@
@using LBPUnion.ProjectLighthouse.Database
@using LBPUnion.ProjectLighthouse.Localization
@using LBPUnion.ProjectLighthouse.Types.Users
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.User
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
bool includeStatus = (bool?)ViewData["IncludeStatus"] ?? false;
string userStatus = includeStatus ? Model.Status.ToTranslatedString(language, timeZone) : "";
await using DatabaseContext database = new();
string userStatus = includeStatus ? Model.GetStatus(database).ToTranslatedString(language, timeZone) : "";
}
<a href="/user/@Model.UserId" title="@userStatus" class="user-link">

View file

@ -3,7 +3,7 @@
@using LBPUnion.ProjectLighthouse.Types.Entities.Level
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
@using LBPUnion.ProjectLighthouse.Types.Moderation.Cases
@model LBPUnion.ProjectLighthouse.Types.Entities.Moderation.ModerationCase
@model LBPUnion.ProjectLighthouse.Types.Entities.Moderation.ModerationCaseEntity
@{
DatabaseContext database = new();
@ -62,7 +62,7 @@
@if (Model.Type.AffectsLevel())
{
Slot? slot = await Model.GetSlotAsync(database);
SlotEntity? slot = await Model.GetSlotAsync(database);
if (slot != null)
{
<p><strong>Affected level:</strong> <a href="/slot/@slot.SlotId">@slot.Name</a></p>
@ -70,7 +70,7 @@
}
else if (Model.Type.AffectsUser())
{
User? user = await Model.GetUserAsync(database);
UserEntity? user = await Model.GetUserAsync(database);
if (user != null)
{
<p><strong>Affected user:</strong> <a href="/user/@user.UserId">@user.Username</a></p>

View file

@ -3,7 +3,8 @@
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
@using LBPUnion.ProjectLighthouse.Types.Levels
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.Photo
@using LBPUnion.ProjectLighthouse.Types.Serialization
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity
@{
string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang;
@ -83,15 +84,17 @@
</p>
}
<div id="hover-subjects-@Model.PhotoId">
@foreach (PhotoSubject subject in Model.PhotoSubjects)
@foreach (PhotoSubjectEntity subject in Model.PhotoSubjects)
{
@await subject.User.ToLink(Html, ViewData, language, timeZone)
}
</div>
@{
PhotoSubject[] subjects = Model.PhotoSubjects.ToArray();
foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username;
GamePhotoSubject[] subjects = Model.PhotoSubjects.Select(GamePhotoSubject.CreateFromEntity).ToArray();
foreach (GamePhotoSubject subject in subjects)
{
subject.Username = Model.PhotoSubjects.Where(ps => ps.UserId == subject.UserId).Select(ps => ps.User.Username).First();
}
}
<script>

View file

@ -1,5 +1,5 @@
@using LBPUnion.ProjectLighthouse.Types.Moderation.Reports
@model LBPUnion.ProjectLighthouse.Types.Entities.Moderation.GriefReport
@model LBPUnion.ProjectLighthouse.Types.Entities.Moderation.GriefReportEntity
@{
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;

View file

@ -3,6 +3,7 @@
@using LBPUnion.ProjectLighthouse.Files
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types.Entities.Level
@using LBPUnion.ProjectLighthouse.Types.Serialization
@{
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
@ -30,7 +31,7 @@
@for(int i = 0; i < Model.Reviews.Count; i++)
{
Review review = Model.Reviews[i];
ReviewEntity review = Model.Reviews[i];
string faceHash = (review.Thumb switch {
-1 => review.Reviewer?.BooHash,
0 => review.Reviewer?.MehHash,

View file

@ -6,10 +6,10 @@
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
@using LBPUnion.ProjectLighthouse.Types.Users
@using Microsoft.EntityFrameworkCore
@model LBPUnion.ProjectLighthouse.Types.Entities.Level.Slot
@model LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity
@{
User? user = (User?)ViewData["User"];
UserEntity? user = (UserEntity?)ViewData["User"];
await using DatabaseContext database = new();

View file

@ -1,6 +1,7 @@
@using LBPUnion.ProjectLighthouse.Database
@using LBPUnion.ProjectLighthouse.Localization
@using LBPUnion.ProjectLighthouse.Types.Users
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.User
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity
@{
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
@ -40,14 +41,22 @@
}
</h1>
}
@{
await using DatabaseContext context = new();
int hearts = Model.GetHeartCount(context);
int comments = Model.GetCommentCount(context);
int levels = Model.GetUsedSlotCount(context);
int photos = Model.GetUploadedPhotoCount(context);
}
<span>
<i>@Model.Status.ToTranslatedString(language, timeZone)</i>
<i>@Model.GetStatus(context).ToTranslatedString(language, timeZone)</i>
</span>
<div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue comment icon" title="Comments"></i> <span>@Model.Comments</span>
<i class="green upload icon" title="Uploaded Levels"></i><span>@Model.UsedSlots</span>
<i class="purple camera icon" title="Uploaded Photos"></i><span>@Model.PhotosByMe</span>
<i class="pink heart icon" title="Hearts"></i> <span>@hearts</span>
<i class="blue comment icon" title="Comments"></i> <span>@comments</span>
<i class="green upload icon" title="Uploaded Levels"></i><span>@levels</span>
<i class="purple camera icon" title="Uploaded Photos"></i><span>@photos</span>
</div>
</div>
</div>