From fde5505b5feca20572588a9b44d9afb756204eb2 Mon Sep 17 00:00:00 2001 From: LumaLivy <7350336+LumaLivy@users.noreply.github.com> Date: Fri, 19 Nov 2021 21:47:08 -0500 Subject: [PATCH] Add basic level review support --- .../Controllers/ReviewController.cs | 179 ++++++++++++++++++ .../Controllers/SlotsController.cs | 4 +- ProjectLighthouse/Database.cs | 3 + ProjectLighthouse/Types/Levels/Slot.cs | 22 ++- .../Types/Reviews/RatedReview.cs | 24 +++ ProjectLighthouse/Types/Reviews/Review.cs | 101 ++++++++++ ProjectLighthouse/Types/User.cs | 13 +- 7 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 ProjectLighthouse/Types/Reviews/RatedReview.cs create mode 100644 ProjectLighthouse/Types/Reviews/Review.cs diff --git a/ProjectLighthouse/Controllers/ReviewController.cs b/ProjectLighthouse/Controllers/ReviewController.cs index c50435af..dc04da77 100644 --- a/ProjectLighthouse/Controllers/ReviewController.cs +++ b/ProjectLighthouse/Controllers/ReviewController.cs @@ -1,8 +1,15 @@ #nullable enable using System; +using System.IO; +using System.Xml.Serialization; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Types.Reviews; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -74,5 +81,177 @@ namespace LBPUnion.ProjectLighthouse.Controllers return this.Ok(); } + [HttpPost("postReview/user/{slotId:int}")] + public async Task PostReview(int slotId) { + User? user = await this.database.UserFromRequest(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 (review == null) { + review = new(); + review.SlotId = slotId; + review.ReviewerId = user.UserId; + review.DeletedBy = "none"; + + } + review.LabelCollection = newReview.LabelCollection; + review.Text = newReview.Text; + review.Deleted = false; + review.Timestamp = TimeHelper.UnixTimeMilliseconds(); + + // sometimes the game posts a review without also calling dpadrate/user/etc (why??) + 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; + this.database.RatedLevels.Add(ratedLevel); + } + + ratedLevel.Rating = newReview.Thumb; + + + await this.database.SaveChangesAsync(); + + return this.Ok(); + } + + [HttpGet("reviewsFor/user/{slotId:int}")] + public async Task ReviewsFor(int slotId, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10) + { + User? user = await this.database.UserFromRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + Token? token = await this.database.TokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + GameVersion gameVersion = token.GameVersion; + + Random rand = new(); + + IEnumerable reviews = this.database.Reviews.Where(r => r.SlotId == slotId && r.Slot.GameVersion <= gameVersion) + .Include(r => r.Reviewer) + .Include(r => r.Slot) + .AsEnumerable() // performance? Needed for next line (ThumbsUp is not in DB) + .OrderByDescending(r => r.ThumbsUp) + .ThenByDescending(_ => rand.Next()) + .Skip(pageStart - 1) + .Take(pageSize); + + string inner = Enumerable.Aggregate(reviews, string.Empty, (current, review) => { + RatedLevel? ratedLevel = this.database.RatedLevels.FirstOrDefault(r => r.SlotId == slotId && r.UserId == review.ReviewerId); + RatedReview? ratedReview = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId); + + return current + review.Serialize(ratedLevel, ratedReview); + }); + + string response = LbpSerializer.TaggedStringElement("reviews", inner, new Dictionary + { + { + "hint_start", pageStart + pageSize + }, + { + "hint", pageStart // not sure + }, + }); + return this.Ok(response); + } + + [HttpGet("reviewsBy/{username}")] + public async Task ReviewsBy(string username, [FromQuery] int pageStart = 1, [FromQuery] int pageSize = 10) + { + User? user = await this.database.UserFromRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + Token? token = await this.database.TokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + GameVersion gameVersion = token.GameVersion; + + IEnumerable reviews = this.database.Reviews.Where(r => r.Reviewer.Username == username && r.Slot.GameVersion <= gameVersion) + .Include(r => r.Reviewer) + .Include(r => r.Slot) + .AsEnumerable() // performance? + .OrderByDescending(r => r.Timestamp) + .Skip(pageStart - 1) + .Take(pageSize); + + string inner = Enumerable.Aggregate(reviews, 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(ratedLevel, ratedReview); + }); + + string response = LbpSerializer.TaggedStringElement("reviews", inner, new Dictionary + { + { + "hint_start", pageStart + }, + { + "hint", reviews.Last().Timestamp // Seems to be the timestamp of oldest + }, + }); + + return this.Ok(response); + } + + [HttpPost("rateReview/user/{slotId:int}/{username}")] + public async Task RateReview(int slotId, string username, [FromQuery] int rating = 0) { + User? user = await this.database.UserFromRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + User? reviewer = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); + if (reviewer == null) return this.StatusCode(403, ""); + + Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == reviewer.UserId); + if (review == null) return this.StatusCode(403, ""); + + 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; + this.database.RatedReviews.Add(ratedReview); + } + + ratedReview.Thumb = Math.Max(Math.Min(1, rating), -1); + + await this.database.SaveChangesAsync(); + + return this.Ok(); + } + + [HttpPost("deleteReview/user/{slotId:int}/{username}")] + public async Task DeleteReview(int slotId, string username) { + User? reviewer = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); + if (reviewer == null) return this.StatusCode(403, ""); + + Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == reviewer.UserId); + if (review == null) return this.StatusCode(403, ""); + + review.Deleted = true; + review.DeletedBy = "level_author"; // other value is "moderator" + + await this.database.SaveChangesAsync(); + return this.Ok(); + } + + public async Task GetReviewFromBody() + { + this.Request.Body.Position = 0; + string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); + + XmlSerializer serializer = new(typeof(Review)); + Review? review = (Review?)serializer.Deserialize(new StringReader(bodyString)); + + return review; + } + } } \ No newline at end of file diff --git a/ProjectLighthouse/Controllers/SlotsController.cs b/ProjectLighthouse/Controllers/SlotsController.cs index 48e45184..f7cfe67e 100644 --- a/ProjectLighthouse/Controllers/SlotsController.cs +++ b/ProjectLighthouse/Controllers/SlotsController.cs @@ -7,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Settings; +using LBPUnion.ProjectLighthouse.Types.Reviews; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -85,7 +86,8 @@ namespace LBPUnion.ProjectLighthouse.Controllers RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == id && r.UserId == user.UserId); VisitedLevel? visitedLevel = await this.database.VisitedLevels.FirstOrDefaultAsync(r => r.SlotId == id && r.UserId == user.UserId); - return this.Ok(slot.Serialize(ratedLevel, visitedLevel)); + Review? yourReview = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == id && r.ReviewerId == user.UserId); + return this.Ok(slot.Serialize(ratedLevel, visitedLevel, yourReview)); } [HttpGet("slots/lbp2cool")] diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 20898272..0af44ada 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -7,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Settings; +using LBPUnion.ProjectLighthouse.Types.Reviews; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -28,6 +29,8 @@ namespace LBPUnion.ProjectLighthouse public DbSet LastMatches { get; set; } public DbSet VisitedLevels { get; set; } public DbSet RatedLevels { get; set; } + public DbSet Reviews { get; set; } + public DbSet RatedReviews { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseMySql(ServerSettings.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); diff --git a/ProjectLighthouse/Types/Levels/Slot.cs b/ProjectLighthouse/Types/Levels/Slot.cs index f21c7240..dd117b12 100644 --- a/ProjectLighthouse/Types/Levels/Slot.cs +++ b/ProjectLighthouse/Types/Levels/Slot.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Profiles; +using LBPUnion.ProjectLighthouse.Types.Reviews; namespace LBPUnion.ProjectLighthouse.Types.Levels { @@ -193,12 +194,22 @@ namespace LBPUnion.ProjectLighthouse.Types.Levels [XmlElement("leveltype")] public string LevelType { get; set; } = ""; + [NotMapped] + [XmlElement("reviewCount")] + public int ReviewCount { + get { + using Database database = new(); + + return database.Reviews.Count(r => r.SlotId == this.SlotId); + } + } + public string SerializeResources() { return this.Resources.Aggregate("", (current, resource) => current + LbpSerializer.StringElement("resource", resource)); } - public string Serialize(RatedLevel? yourRatingStats = null, VisitedLevel? yourVisitedStats = null) + public string Serialize(RatedLevel? yourRatingStats = null, VisitedLevel? yourVisitedStats = null, Review? yourReview = null) { string slotData = LbpSerializer.StringElement("name", this.Name) + @@ -246,9 +257,12 @@ namespace LBPUnion.ProjectLighthouse.Types.Levels LbpSerializer.StringElement("yourLBP1PlayCount", yourVisitedStats?.PlaysLBP1) + LbpSerializer.StringElement("yourLBP2PlayCount", yourVisitedStats?.PlaysLBP2) + LbpSerializer.StringElement("yourLBP3PlayCount", yourVisitedStats?.PlaysLBP3) + - LbpSerializer.StringElement - ("yourLBPVitaPlayCount", yourVisitedStats?.PlaysLBPVita); // i doubt this is the right name but we'll go with it - + LbpSerializer.StringElement("yourLBPVitaPlayCount", yourVisitedStats?.PlaysLBPVita) + // i doubt this is the right name but we'll go with it + yourReview?.Serialize("yourReview") + + LbpSerializer.StringElement("reviewsEnabled", true) + + LbpSerializer.StringElement("commentsEnabled", false) + + LbpSerializer.StringElement("reviewCount", this.ReviewCount); + return LbpSerializer.TaggedStringElement("slot", slotData, "type", "user"); } } diff --git a/ProjectLighthouse/Types/Reviews/RatedReview.cs b/ProjectLighthouse/Types/Reviews/RatedReview.cs new file mode 100644 index 00000000..1ba9720f --- /dev/null +++ b/ProjectLighthouse/Types/Reviews/RatedReview.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LBPUnion.ProjectLighthouse.Types.Reviews +{ + public class RatedReview + { + // ReSharper disable once UnusedMember.Global + [Key] + public int RatedReviewId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public int ReviewId { get; set; } + + [ForeignKey(nameof(ReviewId))] + public Review Review { get; set; } + + public int Thumb { get; set; } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Reviews/Review.cs b/ProjectLighthouse/Types/Reviews/Review.cs new file mode 100644 index 00000000..a2ee79d0 --- /dev/null +++ b/ProjectLighthouse/Types/Reviews/Review.cs @@ -0,0 +1,101 @@ +#nullable enable +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using LBPUnion.ProjectLighthouse.Types.Levels; +using LBPUnion.ProjectLighthouse.Serialization; +using System.Xml.Serialization; + +namespace LBPUnion.ProjectLighthouse.Types.Reviews +{ + [XmlRoot("review")] + [XmlType("review")] + public class Review + { + // ReSharper disable once UnusedMember.Global + [Key] + public int ReviewId { get; set; } + + [XmlIgnore] + public int ReviewerId { get; set; } + + [ForeignKey(nameof(ReviewerId))] + public User Reviewer { get; set; } + + [XmlElement("slot_id")] + public int SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public Slot Slot { get; set; } + + [XmlElement("timestamp")] + public long Timestamp { get; set; } + + [XmlElement("labels")] + public string LabelCollection { get; set; } + + [NotMapped] + [XmlIgnore] + public string[] Labels { + get => this.LabelCollection.Split(","); + set => this.LabelCollection = string.Join(',', value); + } + + [XmlElement("deleted")] + public Boolean Deleted { get; set; } + + [XmlElement("deleted_by")] + public string DeletedBy { get; set; } // enum ? Needs testing e.g. Moderated/Author/Level Author? etc. + + [XmlElement("text")] + public string Text { get; set; } + + [NotMapped] + [XmlElement("thumb")] + public int Thumb { get; set; } // (unused) -- temp value for getting thumb from review upload body for updating level rating + + [NotMapped] + [XmlElement("thumbsup")] + public int ThumbsUp { + get { + using Database database = new(); + + return database.RatedReviews.Count(r => r.ReviewId == this.ReviewId && r.Thumb == 1); + } + } + [NotMapped] + [XmlElement("thumbsdown")] + public int ThumbsDown { + get { + using Database database = new(); + + return database.RatedReviews.Count(r => r.ReviewId == this.ReviewId && r.Thumb == -1); + } + } + + public string Serialize(RatedLevel? yourLevelRating = null, RatedReview? yourRatingStats = null) { + return this.Serialize("review", yourLevelRating, yourRatingStats); + } + + public string Serialize(string elementOverride, RatedLevel? yourLevelRating = null, RatedReview? yourRatingStats = null) + { + + string reviewData = LbpSerializer.TaggedStringElement("slot_id", this.SlotId, "type", this.Slot.Type) + + LbpSerializer.StringElement("reviewer", this.Reviewer.Username) + + LbpSerializer.StringElement("thumb", yourLevelRating?.Rating) + + LbpSerializer.StringElement("timestamp", this.Timestamp) + + LbpSerializer.StringElement("labels", this.LabelCollection) + + LbpSerializer.StringElement("deleted", this.Deleted) + + LbpSerializer.StringElement("deleted_by", this.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); + + return LbpSerializer.TaggedStringElement(elementOverride, reviewData, "id", this.SlotId + "." + this.Reviewer.Username); + } + } + + +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/User.cs b/ProjectLighthouse/Types/User.cs index 83c0806a..3e87be8f 100644 --- a/ProjectLighthouse/Types/User.cs +++ b/ProjectLighthouse/Types/User.cs @@ -24,13 +24,18 @@ namespace LBPUnion.ProjectLighthouse.Types public string Biography { get; set; } [NotMapped] - public int Reviews => 0; + public int Reviews { + get { + using Database database = new(); + return database.Reviews.Count(r => r.ReviewerId == this.UserId); + } + } [NotMapped] public int Comments { get { using Database database = new(); - return database.Comments.Count(c => c.PosterUserId == this.UserId); + return database.Comments.Count(c => c.TargetUserId == this.UserId); } } @@ -114,8 +119,8 @@ namespace LBPUnion.ProjectLighthouse.Types LbpSerializer.StringElement("pins", this.Pins) + LbpSerializer.StringElement("planets", this.PlanetHash) + LbpSerializer.BlankElement("photos") + - LbpSerializer.StringElement("heartCount", this.Hearts); - this.ClientsConnected.Serialize(); + LbpSerializer.StringElement("heartCount", this.Hearts) + + this.ClientsConnected.Serialize(); return LbpSerializer.TaggedStringElement("user", user, "type", "user"); }