diff --git a/.gitignore b/.gitignore index bb08bcee..4513c4e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,11 @@ riderModule.iml /.idea/.idea.ProjectLighthouse/.idea/dataSources.local.xml *.sln.DotSettings.user /ProjectLighthouse/r/* -/ProjectLighthouse/logs/* +logs /ProjectLighthouse/ProjectLighthouse.csproj.user .vs/ +.vscode/ +.editorconfig lighthouse.config.json gitBranch.txt gitVersion.txt diff --git a/ProjectLighthouse/Controllers/EnterLevelController.cs b/ProjectLighthouse/Controllers/EnterLevelController.cs index ba24c642..3cd45500 100644 --- a/ProjectLighthouse/Controllers/EnterLevelController.cs +++ b/ProjectLighthouse/Controllers/EnterLevelController.cs @@ -62,11 +62,8 @@ namespace LBPUnion.ProjectLighthouse.Controllers { v = await visited.FirstOrDefaultAsync(); } - - if (v == null) - { - return this.NotFound(); - } + + if (v == null) return this.NotFound(); switch (gameVersion) { @@ -119,10 +116,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers v = await visited.FirstOrDefaultAsync(); } - if (v == null) - { - return this.NotFound(); - } + if (v == null) return this.NotFound(); slot.PlaysLBP1++; v.PlaysLBP1++; diff --git a/ProjectLighthouse/Controllers/ReviewController.cs b/ProjectLighthouse/Controllers/ReviewController.cs index fa501d3f..35948771 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,186 @@ namespace LBPUnion.ProjectLighthouse.Controllers return this.Ok(); } + [HttpPost("postReview/user/{slotId:int}")] + public async Task PostReview(int slotId) + { + 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 (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, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); + + if (userAndToken == null) return this.StatusCode(403, ""); + + // ReSharper disable once PossibleInvalidOperationException + User user = userAndToken.Value.Item1; + GameToken gameToken = userAndToken.Value.Item2; + + GameVersion gameVersion = gameToken.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, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); + + if (userAndToken == null) return this.StatusCode(403, ""); + + // ReSharper disable once PossibleInvalidOperationException + User user = userAndToken.Value.Item1; + GameToken gameToken = userAndToken.Value.Item2; + + GameVersion gameVersion = gameToken.GameVersion; + + IEnumerable reviews = this.database.Reviews.Where(r => r.Reviewer.Username == username && r.Slot.GameVersion <= gameVersion) + .Include(r => r.Reviewer) + .Include(r => r.Slot) + .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.UserFromGameRequest(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(400, ""); + + Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == reviewer.UserId); + if (review == null) return this.StatusCode(400, ""); + + 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 = DeletedBy.LevelAuthor; + + 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 01ffe886..6fb7f356 100644 --- a/ProjectLighthouse/Controllers/SlotsController.cs +++ b/ProjectLighthouse/Controllers/SlotsController.cs @@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Helpers; 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; @@ -86,7 +87,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/Controllers/UserController.cs b/ProjectLighthouse/Controllers/UserController.cs index 71fb7fd4..126632b9 100644 --- a/ProjectLighthouse/Controllers/UserController.cs +++ b/ProjectLighthouse/Controllers/UserController.cs @@ -29,7 +29,8 @@ namespace LBPUnion.ProjectLighthouse.Controllers public async Task GetSerializedUser(string username, GameVersion gameVersion = GameVersion.LittleBigPlanet1) { User? user = await this.database.Users.Include(u => u.Location).FirstOrDefaultAsync(u => u.Username == username); - return user?.Serialize(gameVersion); + if (user == null) return ""; + return user.Serialize(gameVersion); } [HttpGet("user/{username}")] @@ -39,7 +40,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers if (token == null) return this.StatusCode(403, ""); string? user = await this.GetSerializedUser(username, token.GameVersion); - if (user == null) return this.NotFound(); + if (string.IsNullOrEmpty(user)) return this.NotFound(); return this.Ok(user); } @@ -51,9 +52,10 @@ namespace LBPUnion.ProjectLighthouse.Controllers if (token == null) return this.StatusCode(403, ""); List serializedUsers = new(); - foreach (string userId in u) + foreach (string username in u) { - serializedUsers.Add(await this.GetSerializedUser(userId, token.GameVersion)); + string? serializedUser = await this.GetSerializedUser(username, token.GameVersion); + if (serializedUser != "") serializedUsers.Add(serializedUser); } string serialized = serializedUsers.Aggregate(string.Empty, (current, user) => user == null ? current : current + user); diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 3a5366cc..8b904eda 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -8,6 +8,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; @@ -30,6 +31,8 @@ namespace LBPUnion.ProjectLighthouse public DbSet LastContacts { get; set; } public DbSet VisitedLevels { get; set; } public DbSet RatedLevels { get; set; } + public DbSet Reviews { get; set; } + public DbSet RatedReviews { get; set; } public DbSet AuthenticationAttempts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) @@ -60,7 +63,7 @@ namespace LBPUnion.ProjectLighthouse return user; } - #nullable enable +#nullable enable public async Task AuthenticateUser(LoginData loginData, string userLocation, string titleId = "") { // TODO: don't use psn name to authenticate @@ -252,6 +255,6 @@ namespace LBPUnion.ProjectLighthouse public async Task PhotoFromSubject(PhotoSubject subject) => await this.Photos.FirstOrDefaultAsync(p => p.PhotoSubjectIds.Contains(subject.PhotoSubjectId.ToString())); - #nullable disable +#nullable disable } } \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20211120025513_LevelReviews.Designer.cs b/ProjectLighthouse/Migrations/20211120025513_LevelReviews.Designer.cs new file mode 100644 index 00000000..64eaac27 --- /dev/null +++ b/ProjectLighthouse/Migrations/20211120025513_LevelReviews.Designer.cs @@ -0,0 +1,752 @@ +// +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20211120025513_LevelReviews")] + partial class LevelReviews + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.HeartedProfile", b => + { + b.Property("HeartedProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("HeartedUserId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedProfileId"); + + b.HasIndex("HeartedUserId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedProfiles"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.HeartedLevel", b => + { + b.Property("HeartedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.QueuedLevel", b => + { + b.Property("QueuedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("QueuedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("QueuedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.RatedLevel", b => + { + b.Property("RatedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("RatingLBP1") + .HasColumnType("double"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.Slot", b => + { + b.Property("SlotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AuthorLabels") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("BackgroundHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstUploaded") + .HasColumnType("bigint"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("IconHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InitiallyLocked") + .HasColumnType("tinyint(1)"); + + b.Property("LastUpdated") + .HasColumnType("bigint"); + + b.Property("Lbp1Only") + .HasColumnType("tinyint(1)"); + + b.Property("LevelType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("MaximumPlayers") + .HasColumnType("int"); + + b.Property("MinimumPlayers") + .HasColumnType("int"); + + b.Property("MoveRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlaysLBP1") + .HasColumnType("int"); + + b.Property("PlaysLBP1Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP1Unique") + .HasColumnType("int"); + + b.Property("PlaysLBP2") + .HasColumnType("int"); + + b.Property("PlaysLBP2Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP2Unique") + .HasColumnType("int"); + + b.Property("PlaysLBP3") + .HasColumnType("int"); + + b.Property("PlaysLBP3Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP3Unique") + .HasColumnType("int"); + + b.Property("PlaysLBPVita") + .HasColumnType("int"); + + b.Property("PlaysLBPVitaComplete") + .HasColumnType("int"); + + b.Property("PlaysLBPVitaUnique") + .HasColumnType("int"); + + b.Property("ResourceCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RootLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Shareable") + .HasColumnType("int"); + + b.Property("SubLevel") + .HasColumnType("tinyint(1)"); + + b.Property("TeamPick") + .HasColumnType("tinyint(1)"); + + b.HasKey("SlotId"); + + b.HasIndex("CreatorId"); + + b.HasIndex("LocationId"); + + b.ToTable("Slots"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.VisitedLevel", b => + { + b.Property("VisitedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PlaysLBP1") + .HasColumnType("int"); + + b.Property("PlaysLBP2") + .HasColumnType("int"); + + b.Property("PlaysLBP3") + .HasColumnType("int"); + + b.Property("PlaysLBPVita") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("VisitedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("VisitedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Photo", b => + { + b.Property("PhotoId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("LargeHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MediumHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PhotoSubjectCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlanHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SmallHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("PhotoId"); + + b.HasIndex("CreatorId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.PhotoSubject", b => + { + b.Property("PhotoSubjectId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bounds") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("PhotoSubjectId"); + + b.HasIndex("UserId"); + + b.ToTable("PhotoSubjects"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Comment", b => + { + b.Property("CommentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("PosterUserId") + .HasColumnType("int"); + + b.Property("TargetUserId") + .HasColumnType("int"); + + b.Property("ThumbsDown") + .HasColumnType("int"); + + b.Property("ThumbsUp") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("CommentId"); + + b.HasIndex("PosterUserId"); + + b.HasIndex("TargetUserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.LastMatch", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("UserId"); + + b.ToTable("LastMatches"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("X") + .HasColumnType("int"); + + b.Property("Y") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => + { + b.Property("RatedReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ReviewId") + .HasColumnType("int"); + + b.Property("Thumb") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatedReviewId"); + + b.HasIndex("ReviewId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedReviews"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.Review", b => + { + b.Property("ReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("DeletedBy") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LabelCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReviewerId") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("Text") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("ReviewId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SlotId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Score", b => + { + b.Property("ScoreId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("PlayerIdCollection") + .HasColumnType("longtext"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("ScoreId"); + + b.HasIndex("SlotId"); + + b.ToTable("Scores"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Token", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("UserLocation") + .HasColumnType("longtext"); + + b.Property("UserToken") + .HasColumnType("longtext"); + + b.HasKey("TokenId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Biography") + .HasColumnType("longtext"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("IconHash") + .HasColumnType("longtext"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Pins") + .HasColumnType("longtext"); + + b.Property("PlanetHash") + .HasColumnType("longtext"); + + b.Property("Username") + .HasColumnType("longtext"); + + b.HasKey("UserId"); + + b.HasIndex("LocationId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.HeartedProfile", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "HeartedUser") + .WithMany() + .HasForeignKey("HeartedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HeartedUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.HeartedLevel", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.QueuedLevel", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.RatedLevel", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.Slot", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Profiles.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Levels.VisitedLevel", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Photo", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.PhotoSubject", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Profiles.Comment", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Poster") + .WithMany() + .HasForeignKey("PosterUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Target") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Poster"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Reviews.Review", "Review") + .WithMany() + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.Review", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Reviewer"); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Score", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.User", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Profiles.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ProjectLighthouse/Migrations/20211120025513_LevelReviews.cs b/ProjectLighthouse/Migrations/20211120025513_LevelReviews.cs new file mode 100644 index 00000000..07d2e4c5 --- /dev/null +++ b/ProjectLighthouse/Migrations/20211120025513_LevelReviews.cs @@ -0,0 +1,105 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + public partial class LevelReviews : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Reviews", + columns: table => new + { + ReviewId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ReviewerId = table.Column(type: "int", nullable: false), + SlotId = table.Column(type: "int", nullable: false), + Timestamp = table.Column(type: "bigint", nullable: false), + LabelCollection = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Deleted = table.Column(type: "tinyint(1)", nullable: false), + DeletedBy = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Text = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Reviews", x => x.ReviewId); + table.ForeignKey( + name: "FK_Reviews_Slots_SlotId", + column: x => x.SlotId, + principalTable: "Slots", + principalColumn: "SlotId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Reviews_Users_ReviewerId", + column: x => x.ReviewerId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RatedReviews", + columns: table => new + { + RatedReviewId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + ReviewId = table.Column(type: "int", nullable: false), + Thumb = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RatedReviews", x => x.RatedReviewId); + table.ForeignKey( + name: "FK_RatedReviews_Reviews_ReviewId", + column: x => x.ReviewId, + principalTable: "Reviews", + principalColumn: "ReviewId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RatedReviews_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_RatedReviews_ReviewId", + table: "RatedReviews", + column: "ReviewId"); + + migrationBuilder.CreateIndex( + name: "IX_RatedReviews_UserId", + table: "RatedReviews", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Reviews_ReviewerId", + table: "Reviews", + column: "ReviewerId"); + + migrationBuilder.CreateIndex( + name: "IX_Reviews_SlotId", + table: "Reviews", + column: "SlotId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RatedReviews"); + + migrationBuilder.DropTable( + name: "Reviews"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index afdbbcbb..df97ee90 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -439,6 +439,69 @@ namespace ProjectLighthouse.Migrations b.ToTable("Locations"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => + { + b.Property("RatedReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ReviewId") + .HasColumnType("int"); + + b.Property("Thumb") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatedReviewId"); + + b.HasIndex("ReviewId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedReviews"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.Review", b => + { + b.Property("ReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("DeletedBy") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LabelCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReviewerId") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("Text") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("ReviewId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SlotId"); + + b.ToTable("Reviews"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Score", b => { b.Property("ScoreId") @@ -699,6 +762,44 @@ namespace ProjectLighthouse.Migrations b.Navigation("Target"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Reviews.Review", "Review") + .WithMany() + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.Review", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Reviewer"); + + b.Navigation("Slot"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Score", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.Levels.Slot", "Slot") diff --git a/ProjectLighthouse/Types/DeletedBy.cs b/ProjectLighthouse/Types/DeletedBy.cs new file mode 100644 index 00000000..02d61ec0 --- /dev/null +++ b/ProjectLighthouse/Types/DeletedBy.cs @@ -0,0 +1,9 @@ +namespace LBPUnion.ProjectLighthouse.Types +{ + public static class DeletedBy + { + public static string Moderator { get => "moderator"; } + public static string LevelAuthor { get => "level_author"; } + // TODO: deletion types for comments (profile etc) + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Levels/Slot.cs b/ProjectLighthouse/Types/Levels/Slot.cs index ff0c580e..42353c32 100644 --- a/ProjectLighthouse/Types/Levels/Slot.cs +++ b/ProjectLighthouse/Types/Levels/Slot.cs @@ -6,6 +6,7 @@ using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Profiles; +using LBPUnion.ProjectLighthouse.Types.Reviews; namespace LBPUnion.ProjectLighthouse.Types.Levels { @@ -194,13 +195,23 @@ 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)) + LbpSerializer.StringElement("sizeOfResources", this.Resources.Sum(FileHelper.ResourceSize)); } - 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) + @@ -248,9 +259,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 6f24a385..44424bbe 100644 --- a/ProjectLighthouse/Types/User.cs +++ b/ProjectLighthouse/Types/User.cs @@ -26,7 +26,12 @@ 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 {