Fixes and improvements from Operation Purge (#267)

* Fixes and improvements from Operation Purge

* Limit level name and description length

* Implement suggestions from code review

Co-authored-by: jvyden <jvyden@jvyden.xyz>
This commit is contained in:
Josh 2022-04-06 17:26:40 -05:00 committed by GitHub
commit 8ef8c204a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 243 additions and 93 deletions

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
@ -78,16 +79,18 @@ public class CommentController : ControllerBase
[HttpPost("postComment/user/{slotId:int}")]
public async Task<IActionResult> PostComment(string? username, int? slotId)
{
User? poster = await this.database.UserFromGameRequest(this.Request);
if (poster == null) return this.StatusCode(403, "");
this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
XmlSerializer serializer = new(typeof(Comment));
Comment? comment = (Comment?)serializer.Deserialize(new StringReader(bodyString));
CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level);
SanitizationHelper.SanitizeStringsInClass(comment);
User? poster = await this.database.UserFromGameRequest(this.Request);
if (poster == null) return this.StatusCode(403, "");
CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level);
if (comment == null) return this.BadRequest();

View file

@ -37,6 +37,8 @@ public class FriendsController : ControllerBase
NPData? npData = (NPData?)serializer.Deserialize(new StringReader(bodyString));
if (npData == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(npData);
List<User> friends = new();
foreach (string friendName in npData.Friends)
{

View file

@ -36,6 +36,8 @@ public class ReportController : ControllerBase
if (report == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(report);
report.Bounds = JsonSerializer.Serialize(report.XmlBounds.Rect, typeof(Rectangle));
report.Players = JsonSerializer.Serialize(report.XmlPlayers, typeof(ReportPlayer[]));
report.Timestamp = TimeHelper.UnixTimeMilliseconds();

View file

@ -44,6 +44,8 @@ public class PhotosController : ControllerBase
Photo? photo = (Photo?)serializer.Deserialize(new StringReader(bodyString));
if (photo == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(photo);
foreach (Photo p in this.database.Photos.Where(p => p.CreatorId == user.UserId))
{
if (p.LargeHash == photo.LargeHash) return this.Ok(); // photo already uplaoded

View file

@ -30,7 +30,7 @@ public class ResourcesController : ControllerBase
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
XmlSerializer serializer = new(typeof(ResourceList));
ResourceList resourceList = (ResourceList)serializer.Deserialize(new StringReader(bodyString));
ResourceList? resourceList = (ResourceList?)serializer.Deserialize(new StringReader(bodyString));
if (resourceList == null) return this.BadRequest();

View file

@ -85,7 +85,14 @@ public class PublishController : ControllerBase
User user = userAndToken.Value.Item1;
GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody();
if (slot?.Location == null) return this.BadRequest();
if (slot == null) return this.BadRequest();
if (slot.Location == null) return this.BadRequest();
if (slot.Description.Length > 200) return this.BadRequest();
if (slot.Name.Length > 100) return this.BadRequest();
foreach (string resource in slot.Resources)
{
@ -219,6 +226,8 @@ public class PublishController : ControllerBase
XmlSerializer serializer = new(typeof(Slot));
Slot? slot = (Slot?)serializer.Deserialize(new StringReader(bodyString));
SanitizationHelper.SanitizeStringsInClass(slot);
return slot;
}

View file

@ -41,10 +41,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null)
{
ratedLevel = new RatedLevel();
ratedLevel.SlotId = slotId;
ratedLevel.UserId = user.UserId;
ratedLevel.Rating = 0;
ratedLevel = new RatedLevel
{
SlotId = slotId,
UserId = user.UserId,
Rating = 0,
};
this.database.RatedLevels.Add(ratedLevel);
}
@ -68,10 +70,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null)
{
ratedLevel = new RatedLevel();
ratedLevel.SlotId = slotId;
ratedLevel.UserId = user.UserId;
ratedLevel.RatingLBP1 = 0;
ratedLevel = new RatedLevel
{
SlotId = slotId,
UserId = user.UserId,
RatingLBP1 = 0,
};
this.database.RatedLevels.Add(ratedLevel);
}
@ -91,18 +95,23 @@ public class ReviewController : ControllerBase
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
Review? newReview = await this.getReviewFromBody();
if (newReview == null) return this.BadRequest();
if (newReview.Text.Length > 100) return this.BadRequest();
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
if (review == null)
{
review = new Review();
review.SlotId = slotId;
review.ReviewerId = user.UserId;
review.DeletedBy = DeletedBy.None;
review.ThumbsUp = 0;
review.ThumbsDown = 0;
review = new Review
{
SlotId = slotId,
ReviewerId = user.UserId,
DeletedBy = DeletedBy.None,
ThumbsUp = 0,
ThumbsDown = 0,
};
this.database.Reviews.Add(review);
}
review.Thumb = newReview.Thumb;
@ -115,10 +124,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null)
{
ratedLevel = new RatedLevel();
ratedLevel.SlotId = slotId;
ratedLevel.UserId = user.UserId;
ratedLevel.RatingLBP1 = 0;
ratedLevel = new RatedLevel
{
SlotId = slotId,
UserId = user.UserId,
RatingLBP1 = 0,
};
this.database.RatedLevels.Add(ratedLevel);
}
@ -149,12 +160,14 @@ public class ReviewController : ControllerBase
.Where(r => r.SlotId == slotId)
.Include(r => r.Reviewer)
.Include(r => r.Slot)
.OrderByDescending(r => r.ThumbsUp)
.OrderByDescending(r => r.ThumbsUp - r.ThumbsDown)
.ThenByDescending(r => r.Timestamp)
.Skip(pageStart - 1)
.Take(pageSize);
string inner = reviews.ToList()
List<Review?> reviewList = reviews.ToList();
string inner = reviewList
.Aggregate
(
string.Empty,
@ -162,10 +175,10 @@ public class ReviewController : ControllerBase
{
if (review == null) return current;
return current + review.Serialize();
RatedReview? yourThumb = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize(null, yourThumb);
}
);
string response = LbpSerializer.TaggedStringElement
(
"reviews",
@ -176,7 +189,7 @@ public class ReviewController : ControllerBase
"hint_start", pageStart + pageSize
},
{
"hint", pageStart // not sure
"hint", reviewList.LastOrDefault()!.Timestamp // not sure
},
}
);
@ -197,22 +210,24 @@ public class ReviewController : ControllerBase
GameVersion gameVersion = gameToken.GameVersion;
IEnumerable<Review?> reviews = this.database.Reviews.ByGameVersion(gameVersion, true)
.Where(r => r.Reviewer.Username == username)
.Include(r => r.Reviewer)
.Include(r => r.Slot)
.Where(r => r.Reviewer!.Username == username)
.OrderByDescending(r => r.Timestamp)
.Skip(pageStart - 1)
.Take(pageSize)
.AsEnumerable();
.Take(pageSize);
string inner = reviews.Aggregate
List<Review?> reviewList = reviews.ToList();
string inner = reviewList.Aggregate
(
string.Empty,
(current, review) =>
{
//RatedLevel? ratedLevel = this.database.RatedLevels.FirstOrDefault(r => r.SlotId == review.SlotId && r.UserId == user.UserId);
//RatedReview? ratedReview = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize( /*, ratedReview*/);
if (review == null) return current;
RatedReview? ratedReview = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize(null, ratedReview);
}
);
@ -226,7 +241,7 @@ public class ReviewController : ControllerBase
"hint_start", pageStart
},
{
"hint", reviews.Last().Timestamp // Seems to be the timestamp of oldest
"hint", reviewList.LastOrDefault()!.Timestamp // Seems to be the timestamp of oldest
},
}
);
@ -249,25 +264,40 @@ public class ReviewController : ControllerBase
RatedReview? ratedReview = await this.database.RatedReviews.FirstOrDefaultAsync(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
if (ratedReview == null)
{
ratedReview = new RatedReview();
ratedReview.ReviewId = review.ReviewId;
ratedReview.UserId = user.UserId;
ratedReview.Thumb = 0;
ratedReview = new RatedReview
{
ReviewId = review.ReviewId,
UserId = user.UserId,
Thumb = 0,
};
this.database.RatedReviews.Add(ratedReview);
await this.database.SaveChangesAsync();
}
int oldThumb = ratedReview.Thumb;
ratedReview.Thumb = Math.Max(Math.Min(1, rating), -1);
int oldRating = ratedReview.Thumb;
ratedReview.Thumb = Math.Clamp(rating, -1, 1);
if (oldRating == ratedReview.Thumb) return this.Ok();
if (oldThumb != ratedReview.Thumb)
// if the user's rating changed then we recount the review's ratings to ensure accuracy
List<RatedReview> reactions = await this.database.RatedReviews.Where(r => r.ReviewId == review.ReviewId).ToListAsync();
int yay = 0;
int boo = 0;
foreach (RatedReview r in reactions)
{
if (oldThumb == -1) review.ThumbsDown--;
else if (oldThumb == 1) review.ThumbsUp--;
if (ratedReview.Thumb == -1) review.ThumbsDown++;
else if (ratedReview.Thumb == 1) review.ThumbsUp++;
switch (r.Thumb)
{
case -1:
boo++;
break;
case 1:
yay++;
break;
}
}
review.ThumbsDown = boo;
review.ThumbsUp = yay;
await this.database.SaveChangesAsync();
return this.Ok();
@ -296,7 +326,7 @@ public class ReviewController : ControllerBase
XmlSerializer serializer = new(typeof(Review));
Review? review = (Review?)serializer.Deserialize(new StringReader(bodyString));
SanitizationHelper.SanitizeStringsInClass(review);
return review;
}
}

View file

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
@ -43,6 +44,8 @@ public class ScoreController : ControllerBase
Score? score = (Score?)serializer.Deserialize(new StringReader(bodyString));
if (score == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(score);
score.SlotId = id;
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Profiles;
@ -93,7 +94,19 @@ public class UserController : ControllerBase
if (update == null) return this.BadRequest();
if (update.Biography != null) user.Biography = update.Biography;
SanitizationHelper.SanitizeStringsInClass(update);
if (update.Biography != null)
{
if (update.Biography.Length > 100) return this.BadRequest();
user.Biography = update.Biography;
}
foreach (string? resource in new[] {update.IconHash, update.YayHash, update.MehHash, update.BooHash, update.PlanetHash,})
{
if (resource != null && !FileHelper.ResourceExists(resource)) return this.BadRequest();
}
if (update.IconHash != null) user.IconHash = update.IconHash;

View file

@ -1,6 +1,7 @@
#nullable enable
using System.Threading.Tasks;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
@ -48,6 +49,8 @@ public class SlotPageController : ControllerBase
return this.Redirect("~/slot/" + id);
}
msg = SanitizationHelper.SanitizeString(msg);
await this.database.PostComment(user, id, CommentType.Level, msg);
Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance);

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Kettu;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Helpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -42,6 +43,8 @@ public class UserPageController : ControllerBase
return this.Redirect("~/user/" + id);
}
msg = SanitizationHelper.SanitizeString(msg);
await this.database.PostComment(user, id, CommentType.Profile, msg);
Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance);

View file

@ -138,6 +138,8 @@ public class Database : DbContext
reaction = newReaction;
}
rating = Math.Clamp(rating, -1, 1);
int oldRating = reaction.Rating;
if (oldRating == rating) return true;

View file

@ -0,0 +1,43 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace LBPUnion.ProjectLighthouse.Helpers;
public static class SanitizationHelper
{
private static readonly Dictionary<string, string> charsToReplace = new() {
{"<", "&lt;"},
{">", "&gt;"},
};
public static void SanitizeStringsInClass(object? instance)
{
if (instance == null) return;
PropertyInfo[] properties = instance.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
if (property.PropertyType != typeof(string)) continue;
string? before = (string?) property.GetValue(instance);
if (before == null) continue;
if (!charsToReplace.Keys.Any(k => before.Contains(k))) continue;
property.SetValue(instance, SanitizeString(before));
}
}
public static string SanitizeString(string input)
{
foreach ((string? key, string? value) in charsToReplace)
{
input = input.Replace(key, value);
}
return input;
}
}

View file

@ -0,0 +1,36 @@
#nullable enable
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reports;
using LBPUnion.ProjectLighthouse.Types.Reviews;
namespace LBPUnion.ProjectLighthouse.Maintenance.MaintenanceJobs;
public class CleanupXmlInjection : IMaintenanceJob
{
private readonly Database database = new();
public string Name() => "Sanitize user content";
public string Description() => "Sanitizes all user-generated strings in levels, reviews, comments, users, and scores to prevent XML injection. Only needs to be run once.";
public async Task Run()
{
foreach (Slot slot in this.database.Slots) SanitizationHelper.SanitizeStringsInClass(slot);
foreach (Review review in this.database.Reviews) SanitizationHelper.SanitizeStringsInClass(review);
foreach (Comment comment in this.database.Comments) SanitizationHelper.SanitizeStringsInClass(comment);
foreach (Score score in this.database.Scores) SanitizationHelper.SanitizeStringsInClass(score);
foreach (User user in this.database.Users) SanitizationHelper.SanitizeStringsInClass(user);
foreach (Photo photo in this.database.Photos) SanitizationHelper.SanitizeStringsInClass(photo);
foreach (GriefReport report in this.database.Reports) SanitizationHelper.SanitizeStringsInClass(report);
await this.database.SaveChangesAsync();
}
}

View file

@ -1,4 +1,5 @@
@page "/slot/{id:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@model LBPUnion.ProjectLighthouse.Pages.SlotPage
@ -31,7 +32,7 @@
<div class="eight wide column">
<div class="ui blue segment">
<h2>Description</h2>
<p>@(string.IsNullOrEmpty(Model.Slot.Description) ? "This level has no description." : Model.Slot.Description)</p>
<p>@HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model.Slot.Description) ? "This level has no description." : Model.Slot.Description)</p>
</div>
</div>
<div class="eight wide column">

View file

@ -1,4 +1,5 @@
@page "/user/{userId:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Pages.UserPage
@ -82,7 +83,7 @@
}
else
{
<p>@Model.ProfileUser.Biography</p>
<p>@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
}
</div>
</div>
@ -120,14 +121,14 @@
@if (!Model.ProfileUser.Banned)
{
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban">
<i class="ban icon"></i>
<span>Ban User</span>
</a>
<br><br>
<div>
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban">
<i class="ban icon"></i>
<span>Ban User</span>
</a>
</div>
<div class="ui fitted hidden divider"></div>
}
@await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser)
</div>
}
}

View file

@ -131,12 +131,12 @@ public class Slot
[XmlIgnore]
[NotMapped]
[JsonIgnore]
public int Hearts => database.HeartedLevels.Count(s => s.SlotId == this.SlotId);
public int Hearts => this.database.HeartedLevels.Count(s => s.SlotId == this.SlotId);
[XmlIgnore]
[NotMapped]
[JsonIgnore]
public int Comments => database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId);
public int Comments => this.database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId);
[XmlIgnore]
[NotMapped]
@ -201,19 +201,19 @@ public class Slot
[NotMapped]
[JsonIgnore]
[XmlElement("thumbsup")]
public int Thumbsup => database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1);
public int Thumbsup => this.database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1);
[NotMapped]
[JsonIgnore]
[XmlElement("thumbsdown")]
public int Thumbsdown => database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1);
public int Thumbsdown => this.database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1);
[NotMapped]
[JsonPropertyName("averageRating")]
[XmlElement("averageRating")]
public double RatingLBP1 {
get {
IQueryable<RatedLevel> ratedLevels = database.RatedLevels.Where(r => r.SlotId == this.SlotId && r.RatingLBP1 > 0);
IQueryable<RatedLevel> ratedLevels = this.database.RatedLevels.Where(r => r.SlotId == this.SlotId && r.RatingLBP1 > 0);
if (!ratedLevels.Any()) return 3.0;
return Enumerable.Average(ratedLevels, r => r.RatingLBP1);
@ -223,7 +223,7 @@ public class Slot
[NotMapped]
[JsonIgnore]
[XmlElement("reviewCount")]
public int ReviewCount => database.Reviews.Count(r => r.SlotId == this.SlotId);
public int ReviewCount => this.database.Reviews.Count(r => r.SlotId == this.SlotId);
[XmlElement("leveltype")]
public string LevelType { get; set; } = "";

View file

@ -1,8 +1,6 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels;
@ -21,19 +19,19 @@ public class Review
public int ReviewerId { get; set; }
[ForeignKey(nameof(ReviewerId))]
public User Reviewer { get; set; }
public User? Reviewer { get; set; }
[XmlElement("slot_id")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public Slot Slot { get; set; }
public Slot? Slot { get; set; }
[XmlElement("timestamp")]
public long Timestamp { get; set; }
[XmlElement("labels")]
public string LabelCollection { get; set; }
public string LabelCollection { get; set; } = "";
[NotMapped]
[XmlIgnore]
@ -49,7 +47,7 @@ public class Review
public DeletedBy DeletedBy { get; set; }
[XmlElement("text")]
public string Text { get; set; }
public string Text { get; set; } = "";
[XmlElement("thumb")]
public int Thumb { get; set; }
@ -66,27 +64,26 @@ public class Review
public string Serialize(string elementOverride, RatedLevel? yourLevelRating = null, RatedReview? yourRatingStats = null)
{
string deletedBy = this.DeletedBy switch
{
DeletedBy.None => "none",
DeletedBy.Moderator => "moderator",
DeletedBy.LevelAuthor => "level_author",
_ => "none",
};
XmlWriterSettings settings = new();
settings.OmitXmlDeclaration = true;
XmlSerializer serializer = new(typeof(DeletedBy));
StringWriter stringWriter = new();
using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings)) serializer.Serialize(xmlWriter, this.DeletedBy);
string deletedBy = stringWriter.ToString();
string reviewData = LbpSerializer.TaggedStringElement("slot_id", this.SlotId, "type", this.Slot.Type) +
LbpSerializer.StringElement("reviewer", this.Reviewer.Username) +
string reviewData = LbpSerializer.TaggedStringElement("slot_id", this.SlotId, "type", this.Slot?.Type) +
LbpSerializer.StringElement("reviewer", this.Reviewer?.Username) +
LbpSerializer.StringElement("thumb", this.Thumb) +
LbpSerializer.StringElement("timestamp", this.Timestamp) +
LbpSerializer.StringElement("labels", this.LabelCollection) +
LbpSerializer.StringElement("deleted", this.Deleted) +
deletedBy +
LbpSerializer.StringElement("deleted_by", deletedBy) +
LbpSerializer.StringElement("text", this.Text) +
LbpSerializer.StringElement("thumbsup", this.ThumbsUp) +
LbpSerializer.StringElement("thumbsdown", this.ThumbsDown) +
LbpSerializer.StringElement("yourthumb", yourRatingStats?.Thumb == null ? 0 : yourRatingStats?.Thumb);
LbpSerializer.StringElement("yourthumb", yourRatingStats?.Thumb ?? 0);
return LbpSerializer.TaggedStringElement(elementOverride, reviewData, "id", this.SlotId + "." + this.Reviewer.Username);
return LbpSerializer.TaggedStringElement(elementOverride, reviewData, "id", this.SlotId + "." + this.Reviewer?.Username);
}
}

View file

@ -7,23 +7,23 @@ namespace LBPUnion.ProjectLighthouse.Types;
public class UserUpdate
{
[XmlElement("location")]
public Location? Location;
public Location? Location { get; set; }
[XmlElement("biography")]
public string? Biography;
public string? Biography { get; set; }
[XmlElement("icon")]
public string? IconHash;
public string? IconHash { get; set; }
[XmlElement("planets")]
public string? PlanetHash;
public string? PlanetHash { get; set; }
[XmlElement("yay2")]
public string? YayHash;
public string? YayHash { get; set; }
[XmlElement("meh2")]
public string? MehHash;
public string? MehHash { get; set; }
[XmlElement("boo2")]
public string? BooHash;
public string? BooHash { get; set; }
}