Add level comments and yays and boos for all comments

This commit is contained in:
Slendy 2022-02-05 02:53:03 -06:00
commit 19a29ca328
15 changed files with 370 additions and 46 deletions

View file

@ -1,4 +1,5 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -7,6 +8,7 @@ using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -24,37 +26,130 @@ public class CommentController : ControllerBase
this.database = database;
}
[HttpGet("userComments/{username}")]
public async Task<IActionResult> GetComments(string username)
[HttpPost("rateUserComment/{username}")]
[HttpPost("rateComment/user/{slotId:int}")]
public async Task<IActionResult> RateComment([FromQuery] int commentId, [FromQuery] int rating, string? username, int? slotId)
{
List<Comment> comments = await this.database.Comments.Include
(c => c.Target)
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
Comment? comment = await this.database.Comments.Include(c => c.Poster).FirstOrDefaultAsync(c => commentId == c.CommentId);
if (comment == null) return this.BadRequest();
Reaction? reaction = await this.database.Reactions.FirstOrDefaultAsync(r => r.UserId == user.UserId && r.TargetId == commentId);
if (reaction == null)
{
Reaction newReaction = new Reaction()
{
UserId = user.UserId,
TargetId = commentId,
Rating = 0,
};
this.database.Reactions.Add(newReaction);
await this.database.SaveChangesAsync();
reaction = newReaction;
}
int oldRating = reaction.Rating;
if (oldRating == rating) return this.Ok();
reaction.Rating = rating;
// if rating changed then we count the number of reactions to ensure accuracy
List<Reaction> reactions = await this.database.Reactions
.Where(c => c.TargetId == commentId)
.ToListAsync();
int yay = 0;
int boo = 0;
foreach (Reaction r in reactions)
{
switch (r.Rating)
{
case -1:
boo++;
break;
case 1:
yay++;
break;
}
}
comment.ThumbsDown = boo;
comment.ThumbsUp = yay;
await this.database.SaveChangesAsync();
return this.Ok();
}
[HttpGet("comments/user/{slotId:int}")]
[HttpGet("userComments/{username}")]
public async Task<IActionResult> GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, int? slotId)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
int targetId = slotId.GetValueOrDefault();
CommentType type = CommentType.Level;
if (!string.IsNullOrWhiteSpace(username))
{
targetId = this.database.Users.First(u => u.Username.Equals(username)).UserId;
type = CommentType.Profile;
}
List<Comment> comments = await this.database.Comments
.Include(c => c.Poster)
.Where(c => c.Target.Username == username)
.Where(c => c.TargetId == targetId && c.Type == type)
.OrderByDescending(c => c.Timestamp)
.Skip(pageStart - 1)
.Take(Math.Min(pageSize,
30))
.ToListAsync();
string outputXml = comments.Aggregate(string.Empty, (current, comment) => current + comment.Serialize());
string outputXml = comments.Aggregate(string.Empty, (current, comment) => current +
comment.Serialize(this.getReaction(user.UserId, comment.CommentId).Result));
return this.Ok(LbpSerializer.StringElement("comments", outputXml));
}
[HttpPost("postUserComment/{username}")]
public async Task<IActionResult> PostComment(string username)
public async Task<int> getReaction(int userId, int commentId)
{
this.Request.Body.Position = 0;
Reaction? reaction = await this.database.Reactions.FirstOrDefaultAsync(r => r.UserId == userId && r.TargetId == commentId);
if (reaction == null) return 0;
return reaction.Rating;
}
[HttpPost("postUserComment/{username}")]
[HttpPost("postComment/user/{slotId:int}")]
public async Task<IActionResult> PostComment(string? username, int? slotId)
{
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));
Comment? comment = (Comment?) serializer.Deserialize(new StringReader(bodyString));
CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level);
User? poster = await this.database.UserFromGameRequest(this.Request);
if (poster == null) return this.StatusCode(403, "");
User? target = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (comment == null || target == null) return this.BadRequest();
if (comment == null) return this.BadRequest();
int targetId = slotId.GetValueOrDefault();
if (type == CommentType.Profile)
{
User? target = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (target == null) return this.BadRequest();
targetId = target.UserId;
}
else
{
Slot? target = await this.database.Slots.FirstOrDefaultAsync(u => u.SlotId == slotId);
if (target == null) return this.BadRequest();
}
comment.PosterUserId = poster.UserId;
comment.TargetUserId = target.UserId;
comment.TargetId = targetId;
comment.Type = type;
comment.Timestamp = TimeHelper.UnixTimeMilliseconds();
@ -64,17 +159,40 @@ public class CommentController : ControllerBase
}
[HttpPost("deleteUserComment/{username}")]
public async Task<IActionResult> DeleteComment([FromQuery] int commentId, string username)
[HttpPost("deleteComment/user/{slotId:int}")]
public async Task<IActionResult> DeleteComment([FromQuery] int commentId, string? username, int? slotId)
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
Comment? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId);
if (comment == null) return this.NotFound();
// if you are not the poster
if (comment.PosterUserId != user.UserId)
{
if (comment.Type == CommentType.Profile)
{
// if you aren't the poster and aren't the profile owner
if (comment.TargetId != user.UserId)
{
return this.StatusCode(403, "");
}
}
else
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == comment.TargetId);
// if you aren't the creator of the level
if (slot == null || slot.CreatorId != user.UserId || slotId.GetValueOrDefault() != slot.SlotId)
{
return this.StatusCode(403, "");
}
}
}
if (comment.TargetUserId != user.UserId && comment.PosterUserId != user.UserId) return this.StatusCode(403, "");
comment.Deleted = true;
comment.DeletedBy = user.Username;
comment.DeletedType = "user";
this.database.Comments.Remove(comment);
await this.database.SaveChangesAsync();
return this.Ok();

View file

@ -36,6 +36,7 @@ public class Database : DbContext
public DbSet<RatedReview> RatedReviews { get; set; }
public DbSet<UserApprovedIpAddress> UserApprovedIpAddresses { get; set; }
public DbSet<DatabaseCategory> CustomCategories { get; set; }
public DbSet<Reaction> Reactions { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
@ -272,6 +273,7 @@ public class Database : DbContext
this.Comments.RemoveRange(this.Comments.Where(c => c.PosterUserId == user.UserId));
this.Reviews.RemoveRange(this.Reviews.Where(r => r.ReviewerId == user.UserId));
this.Photos.RemoveRange(this.Photos.Where(p => p.CreatorId == user.UserId));
this.Reactions.RemoveRange(this.Reactions.Where(p => p.UserId == user.UserId));
this.Users.Remove(user);

View file

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
public partial class CommentRefactor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Comments_Slots_TargetId",
table: "Comments");
migrationBuilder.DropForeignKey(
name: "FK_Comments_Users_TargetId",
table: "Comments");
migrationBuilder.DropIndex(
name: "IX_Comments_TargetId",
table: "Comments");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Comments_TargetId",
table: "Comments",
column: "TargetId");
migrationBuilder.AddForeignKey(
name: "FK_Comments_Slots_TargetId",
table: "Comments",
column: "TargetId",
principalTable: "Slots",
principalColumn: "SlotId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Comments_Users_TargetId",
table: "Comments",
column: "TargetId",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -412,13 +412,22 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("Deleted")
.HasColumnType("tinyint(1)");
b.Property<string>("DeletedBy")
.HasColumnType("longtext");
b.Property<string>("DeletedType")
.HasColumnType("longtext");
b.Property<string>("Message")
.HasColumnType("longtext");
b.Property<int>("PosterUserId")
.HasColumnType("int");
b.Property<int>("TargetUserId")
b.Property<int>("TargetId")
.HasColumnType("int");
b.Property<int>("ThumbsDown")
@ -430,12 +439,13 @@ namespace ProjectLighthouse.Migrations
b.Property<long>("Timestamp")
.HasColumnType("bigint");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("CommentId");
b.HasIndex("PosterUserId");
b.HasIndex("TargetUserId");
b.ToTable("Comments");
});
@ -473,6 +483,26 @@ namespace ProjectLighthouse.Migrations
b.ToTable("Locations");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reaction", b =>
{
b.Property<int>("RatingId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<int>("TargetId")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("RatingId");
b.ToTable("Reactions");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b =>
{
b.Property<int>("RatedReviewId")
@ -829,15 +859,7 @@ namespace ProjectLighthouse.Migrations
.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 =>

View file

@ -10,11 +10,13 @@
<script>
function onSubmit(form) {
const password = form['password'];
const confirmPassword = form['confirmPassword'];
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirmPassword");
const passwordSubmit = document.getElementById("password-submit");
const confirmPasswordSubmit = document.getElementById("confirmPassword-submit");
password.value = sha256(password.value);
confirmPassword.value = sha256(confirmPassword.value);
passwordSubmit.value = sha256(passwordInput.value);
confirmPasswordSubmit.value = sha256(confirmPasswordInput.value);
return true;
}
@ -35,12 +37,14 @@
<div class="ui left labeled input">
<label for="password" class="ui blue label">Password: </label>
<input type="password" name="password" id="password">
<input type="password" id="password">
<input type="hidden" id="password-submit" name="password">
</div><br><br>
<div class="ui left labeled input">
<label for="password" class="ui blue label">Confirm Password: </label>
<input type="password" name="confirmPassword" id="confirmPassword">
<input type="password" id="confirmPassword">
<input type="hidden" id="confirmPassword-submit" name="confirmPassword">
</div><br><br><br>
<input type="submit" value="Reset password and continue" id="submit" class="ui green button"><br>

View file

@ -1,5 +1,8 @@
@page "/slot/{id:int}"
@using System.IO
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types.Profiles
@model LBPUnion.ProjectLighthouse.Pages.SlotPage
@{
@ -54,6 +57,36 @@
</div>
</div>
</div>
<div class="ui yellow segment">
<h1>Comments</h1>
@if (Model.Comments.Count == 0)
{
<p>There are no comments.</p>
}
@foreach (Comment comment in Model.Comments!)
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
<div>
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
@if (comment.Deleted)
{
<i><span>@decodedMessage</span></i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>
<div class="ui divider"></div>
</div>
}
</div>
@if (Model.User != null && Model.User.IsAdmin)
{
<div class="ui yellow segment">

View file

@ -1,8 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -10,6 +14,7 @@ namespace LBPUnion.ProjectLighthouse.Pages;
public class SlotPage : BaseLayout
{
public List<Comment> Comments;
public Slot Slot;
public SlotPage([NotNull] Database database) : base(database)
@ -20,6 +25,12 @@ public class SlotPage : BaseLayout
Slot? slot = await this.Database.Slots.Include(s => s.Creator).FirstOrDefaultAsync(s => s.SlotId == id);
if (slot == null) return this.NotFound();
this.Comments = await this.Database.Comments.Include(p => p.Poster)
.OrderByDescending(p => p.Timestamp)
.Where(c => c.TargetId == id && c.Type == CommentType.Level)
.Take(50)
.ToListAsync();
this.Slot = slot;
return this.Page();

View file

@ -126,11 +126,19 @@
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.Message, messageWriter);
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
<div>
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
<span>@decodedMessage</span>
@if (comment.Deleted)
{
<i><span>@decodedMessage</span></i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>

View file

@ -30,9 +30,8 @@ public class UserPage : BaseLayout
this.Photos = await this.Database.Photos.OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync();
this.Comments = await this.Database.Comments.Include
(p => p.Poster)
.Include(p => p.Target)
.OrderByDescending(p => p.Timestamp)
.Where(p => p.TargetUserId == userId)
.Where(p => p.TargetId == userId && p.Type == CommentType.Profile)
.Take(50)
.ToListAsync();

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types;
public enum CommentType
{
Profile = 0,
Level = 1,
}

View file

@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reviews;
using LBPUnion.ProjectLighthouse.Types.Settings;
namespace LBPUnion.ProjectLighthouse.Types.Levels;
@ -121,6 +122,19 @@ public class Slot
}
}
[XmlIgnore]
[NotMapped]
[JsonIgnore]
public int Comments
{
get
{
using Database database = new();
return database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId);
}
}
[XmlIgnore]
[NotMapped]
public int Plays => this.PlaysLBP1 + this.PlaysLBP2 + this.PlaysLBP3 + this.PlaysLBPVita;
@ -251,6 +265,7 @@ public class Slot
LbpSerializer.StringElement("icon", this.IconHash) +
LbpSerializer.StringElement("rootLevel", this.RootLevel) +
LbpSerializer.StringElement("authorLabels", this.AuthorLabels) +
LbpSerializer.StringElement("labels", this.AuthorLabels) +
this.SerializeResources() +
LbpSerializer.StringElement("location", this.Location?.Serialize()) +
LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) +
@ -266,6 +281,7 @@ public class Slot
LbpSerializer.StringElement("mmpick", this.TeamPick) +
LbpSerializer.StringElement("heartCount", this.Hearts) +
LbpSerializer.StringElement("playCount", this.Plays) +
LbpSerializer.StringElement("commentCount", this.Comments) +
LbpSerializer.StringElement("uniquePlayCount", this.PlaysLBP2Unique) + // ??? good naming scheme lol
LbpSerializer.StringElement("completionCount", this.PlaysComplete) +
LbpSerializer.StringElement("lbp1PlayCount", this.PlaysLBP1) +
@ -277,7 +293,7 @@ public class Slot
LbpSerializer.StringElement("lbp3PlayCount", this.PlaysLBP3) +
LbpSerializer.StringElement("lbp3CompletionCount", this.PlaysLBP3Complete) +
LbpSerializer.StringElement("lbp3UniquePlayCount", this.PlaysLBP3Unique) +
LbpSerializer.StringElement("vitaCrossControlRequired", CrossControllerRequired) +
LbpSerializer.StringElement("vitaCrossControlRequired", this.CrossControllerRequired) +
LbpSerializer.StringElement("thumbsup", this.Thumbsup) +
LbpSerializer.StringElement("thumbsdown", this.Thumbsdown) +
LbpSerializer.StringElement("averageRating", this.RatingLBP1) +
@ -290,8 +306,8 @@ public class Slot
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("reviewsEnabled", ServerSettings.Instance.LevelReviewsEnabled) +
LbpSerializer.StringElement("commentsEnabled", ServerSettings.Instance.LevelCommentsEnabled) +
LbpSerializer.StringElement("reviewCount", this.ReviewCount);
return LbpSerializer.TaggedStringElement("slot", slotData, "type", "user");

View file

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Serialization;
@ -15,27 +16,60 @@ public class Comment
public int PosterUserId { get; set; }
public int TargetUserId { get; set; }
public int TargetId { get; set; }
[ForeignKey(nameof(PosterUserId))]
public User Poster { get; set; }
[ForeignKey(nameof(TargetUserId))]
public User Target { get; set; }
public bool Deleted { get; set; }
public string DeletedType { get; set; }
public string DeletedBy { get; set; }
public long Timestamp { get; set; }
[XmlElement("message")]
public string Message { get; set; }
public CommentType Type { get; set; }
public int ThumbsUp { get; set; }
public int ThumbsDown { get; set; }
public string getComment()
{
if (!this.Deleted)
{
return this.Message;
}
if (this.DeletedBy == this.Poster.Username)
{
return "This comment has been deleted by the author.";
}
else
{
using Database database = new();
User deletedBy = database.Users.FirstOrDefault(u => u.Username == this.DeletedBy);
if (deletedBy != null && deletedBy.UserId == this.TargetId)
{
return "This comment has been deleted by the player.";
}
}
return "This comment has been deleted.";
}
private string serialize()
=> LbpSerializer.StringElement("id", this.CommentId) +
LbpSerializer.StringElement("npHandle", this.Poster.Username) +
LbpSerializer.StringElement("timestamp", this.Timestamp) +
LbpSerializer.StringElement("message", this.Message) +
(this.Deleted ? LbpSerializer.StringElement("deleted", true) +
LbpSerializer.StringElement("deletedBy", this.DeletedBy) +
LbpSerializer.StringElement("deletedType", this.DeletedBy) : "") +
LbpSerializer.StringElement("thumbsup", this.ThumbsUp) +
LbpSerializer.StringElement("thumbsdown", this.ThumbsDown);

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace LBPUnion.ProjectLighthouse.Types;
public class Reaction
{
[Key]
public int RatingId { get; set; }
public int UserId { get; set; }
public int TargetId { get; set; }
public int Rating { get; set; }
}

View file

@ -13,7 +13,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings;
[Serializable]
public class ServerSettings
{
public const int CurrentConfigVersion = 18; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
public const int CurrentConfigVersion = 19; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
private static FileSystemWatcher fileWatcher;
static ServerSettings()
{
@ -132,6 +132,12 @@ public class ServerSettings
public int PhotosQuota { get; set; } = 500;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;
public bool LevelReviewsEnabled { get; set; } = true;
public bool GoogleAnalyticsEnabled { get; set; }
public string GoogleAnalyticsId { get; set; } = "";

View file

@ -61,7 +61,7 @@ public class User
public int Comments {
get {
using Database database = new();
return database.Comments.Count(c => c.TargetUserId == this.UserId);
return database.Comments.Count(c => c.Type == CommentType.Profile && c.TargetId == this.UserId);
}
}
@ -180,7 +180,7 @@ public class User
LbpSerializer.StringElement("commentCount", this.Comments) +
LbpSerializer.StringElement("photosByMeCount", this.PhotosByMe) +
LbpSerializer.StringElement("photosWithMeCount", this.PhotosWithMe) +
LbpSerializer.StringElement("commentsEnabled", "true") +
LbpSerializer.StringElement("commentsEnabled", ServerSettings.Instance.ProfileCommentsEnabled) +
LbpSerializer.StringElement("location", this.Location.Serialize()) +
LbpSerializer.StringElement("favouriteSlotCount", this.HeartedLevels) +
LbpSerializer.StringElement("favouriteUserCount", this.HeartedUsers) +