Merge remote-tracking branch 'upstream/main' into Pullrequests

This commit is contained in:
FridiNaTor1 2022-02-14 09:40:38 +00:00
commit f61b19e331
37 changed files with 609 additions and 119 deletions

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

2
.gitignore vendored
View file

@ -43,5 +43,3 @@ backup*
*.tmp
*.bin
*.png
*.jpg

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{fomantic-ui, sha256}" />
<file url="PROJECT" libraries="{api, fomantic-ui, sha256}" />
</component>
</project>

View file

@ -9,8 +9,8 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -20,7 +20,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -9,20 +9,20 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0"/>
<PackageReference Include="Selenium.WebDriver" Version="4.1.0"/>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="96.0.4664.4500"/>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="98.0.4758.8000"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -14,8 +14,8 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -25,7 +25,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

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

@ -74,7 +74,7 @@ public class ReviewController : ControllerBase
this.database.RatedLevels.Add(ratedLevel);
}
ratedLevel.Rating = Math.Max(Math.Min(1, rating), -1);
ratedLevel.Rating = Math.Clamp(rating, -1, 1);
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
if (review != null) review.Thumb = ratedLevel.Rating;
@ -161,12 +161,12 @@ public class ReviewController : ControllerBase
yourReview = new Review();
yourReview.ReviewerId = user.UserId;
yourReview.Reviewer = user;
yourReview.Thumb = ratedLevel?.Rating == null ? 0 : ratedLevel.Rating;
yourReview.Thumb = ratedLevel?.Rating ?? 0;
yourReview.Slot = slot;
yourReview.SlotId = slotId;
yourReview.Deleted = false;
yourReview.DeletedBy = DeletedBy.None;
yourReview.Text = "You haven't reviewed this level yet. Edit this blank review to upload one!";
yourReview.Text = "You haven't reviewed this level yet. Edit this to write one!";
yourReview.LabelCollection = "";
yourReview.Timestamp = TimeHelper.UnixTimeMilliseconds();
}

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,37 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Newtonsoft.Json.Linq;
namespace LBPUnion.ProjectLighthouse.Helpers;
public static class CaptchaHelper
{
private static readonly HttpClient client = new()
{
BaseAddress = new Uri("https://hcaptcha.com"),
};
public static async Task<bool> Verify(string token)
{
if (!ServerSettings.Instance.HCaptchaEnabled) return true;
List<KeyValuePair<string, string>> payload = new()
{
new("secret", ServerSettings.Instance.HCaptchaSecret),
new("response", token),
};
HttpResponseMessage response = await client.PostAsync("/siteverify", new FormUrlEncodedContent(payload));
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// We only really care about the success result, nothing else that hcaptcha sends us, so lets only parse that.
bool success = bool.Parse(JObject.Parse(responseBody)["success"]?.ToString() ?? "false");
return success;
}
}

View file

@ -1,5 +1,9 @@
#nullable enable
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace LBPUnion.ProjectLighthouse.Helpers.Extensions;
@ -11,4 +15,17 @@ public static class RequestExtensions
("Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
public static bool IsMobile(this HttpRequest request) => mobileCheck.IsMatch(request.Headers[HeaderNames.UserAgent].ToString());
public static async Task<bool> CheckCaptchaValidity(this HttpRequest request)
{
if (ServerSettings.Instance.HCaptchaEnabled)
{
bool gotCaptcha = request.Form.TryGetValue("h-captcha-response", out StringValues values);
if (!gotCaptcha) return false;
if (!await CaptchaHelper.Verify(values[0])) return false;
}
return true;
}
}

View file

@ -21,6 +21,7 @@ public static class FileHelper
return file.FileType switch
{
LbpFileType.MotionRecording => true,
LbpFileType.FileArchive => false,
LbpFileType.Painting => true,
LbpFileType.Unknown => false,
@ -56,17 +57,18 @@ public static class FileHelper
return Encoding.ASCII.GetString(header) switch
{
"REC" => LbpFileType.MotionRecording,
"PTG" => LbpFileType.Painting,
"TEX" => LbpFileType.Texture,
"FSH" => LbpFileType.Script,
"VOP" => LbpFileType.Voice,
"LVL" => LbpFileType.Level,
"PLN" => LbpFileType.Plan,
_ => determineFileTypePartTwoWeirdName(reader),
_ => readAlternateHeader(reader),
};
}
private static LbpFileType determineFileTypePartTwoWeirdName(BinaryReader reader)
private static LbpFileType readAlternateHeader(BinaryReader reader)
{
reader.BaseStream.Position = 0;

View file

@ -0,0 +1,114 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220205132152_CommentRefactor")]
public partial class CommentRefactor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Comments_Users_TargetUserId",
table: "Comments");
migrationBuilder.DropIndex(
name: "IX_Comments_TargetUserId",
table: "Comments");
migrationBuilder.RenameColumn(
name: "TargetUserId",
table: "Comments",
newName: "TargetId");
migrationBuilder.AddColumn<bool>(
name: "Deleted",
table: "Comments",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "DeletedBy",
table: "Comments",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "DeletedType",
table: "Comments",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<int>(
name: "Type",
table: "Comments",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "Reactions",
columns: table => new
{
RatingId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
TargetId = table.Column<int>(type: "int", nullable: false),
Rating = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Reactions", x => x.RatingId);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Reactions");
migrationBuilder.DropColumn(
name: "Deleted",
table: "Comments");
migrationBuilder.DropColumn(
name: "DeletedBy",
table: "Comments");
migrationBuilder.DropColumn(
name: "DeletedType",
table: "Comments");
migrationBuilder.DropColumn(
name: "Type",
table: "Comments");
migrationBuilder.RenameColumn(
name: "TargetId",
table: "Comments",
newName: "TargetUserId");
migrationBuilder.CreateIndex(
name: "IX_Comments_TargetUserId",
table: "Comments",
column: "TargetUserId");
migrationBuilder.AddForeignKey(
name: "FK_Comments_Users_TargetUserId",
table: "Comments",
column: "TargetUserId",
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

@ -35,19 +35,24 @@ else
<div class="ui blue segment">
<h3>@command.Name()</h3>
<form>
<input type="text" name="command" style="display: none;" value="@command.FirstAlias">
@if (command.RequiredArgs() > 0)
{
<div class="ui input" style="width: @(Model.Request.IsMobile() ? 100 : 30)%;">
<div class="ui left action input" style="width: 100%">
<button type="submit" class="ui green button">
<i class="play icon"></i>
Execute
</button>
<input type="text" name="args" placeholder="@command.Arguments()">
</div>
<br>
<br>
}
<input type="text" name="command" style="display: none;" value="@command.FirstAlias">
<button type="submit" class="ui green button">
<i class="play icon"></i>
Execute
</button>
else
{
<button type="submit" class="ui green button">
<i class="play icon"></i>
Execute
</button>
}
</form>
</div>
}

View file

@ -74,17 +74,6 @@
gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId');
</script>
}
<style>
canvas.photo-subjects {
opacity: 1;
transition: opacity 0.3s;
}
canvas.hide-subjects {
opacity: 0;
}
</style>
</head>
<body>
<div class="pageContainer">

View file

@ -50,6 +50,11 @@
</div>
</div>
@if (ServerSettings.Instance.HCaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}
<input type="submit" value="Log in" id="submit" class="ui blue button">
@if (ServerSettings.Instance.RegistrationEnabled)
{

View file

@ -1,11 +1,14 @@
#nullable enable
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -18,8 +21,6 @@ public class LoginForm : BaseLayout
public string Error { get; private set; }
public bool WasLoginRequest { get; private set; }
[UsedImplicitly]
public async Task<IActionResult> OnPost(string username, string password)
{
@ -35,6 +36,12 @@ public class LoginForm : BaseLayout
return this.Page();
}
if (!await Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
return this.Page();
}
User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null)
{
@ -66,7 +73,17 @@ public class LoginForm : BaseLayout
this.Database.WebTokens.Add(webToken);
await this.Database.SaveChangesAsync();
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
this.Response.Cookies.Append
(
"LighthouseToken",
webToken.UserToken,
new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
}
);
Logger.Log($"User {user.Username} (id: {user.UserId}) successfully logged in on web", LoggerLevelLogin.Instance);
if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");

View file

@ -0,0 +1,6 @@
@using LBPUnion.ProjectLighthouse.Types.Settings
@if (ServerSettings.Instance.HCaptchaEnabled)
{
<div class="h-captcha" data-sitekey="@ServerSettings.Instance.HCaptchaSiteKey"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
}

View file

@ -32,7 +32,9 @@
@{
int size = isMobile ? 50 : 100;
}
<div class="cardIcon slotCardIcon" style="background-image: url('/gameAssets/@iconHash'); min-width: @(size)px; width: @(size)px; height: @(size)px">
<div>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px">
</div>
<div class="cardStats">
@if (showLink)

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,4 +1,5 @@
@page "/register"
@using LBPUnion.ProjectLighthouse.Types.Settings
@model LBPUnion.ProjectLighthouse.Pages.RegisterForm
@{
@ -60,5 +61,10 @@
</div>
</div>
@if (ServerSettings.Instance.HCaptchaEnabled)
{
@await Html.PartialAsync("Partials/CaptchaPartial")
}
<input type="submit" value="Register" id="submit" class="ui green button">
</form>

View file

@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
@ -42,13 +43,18 @@ public class RegisterForm : BaseLayout
return this.Page();
}
bool userExists = await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null;
if (userExists)
if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
{
this.Error = "The username you've chosen is already taken.";
return this.Page();
}
if (!await Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
return this.Page();
}
User user = await this.Database.CreateUser(username, HashHelper.BCryptHash(password));
WebToken webToken = new()

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

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<TargetFramework>net6.0</TargetFramework>
@ -15,18 +14,18 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2"/>
<PackageReference Include="DDSReader" Version="1.0.8-pre"/>
<PackageReference Include="Discord.Net.Webhook" Version="3.2.0"/>
<PackageReference Include="InfluxDB.Client" Version="3.2.0"/>
<PackageReference Include="Discord.Net.Webhook" Version="3.3.0"/>
<PackageReference Include="InfluxDB.Client" Version="3.3.0"/>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Kettu" Version="1.2.1"/>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PackageReference Include="Kettu" Version="1.2.2"/>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1"/>
<PackageReference Include="SharpZipLib" Version="1.3.3"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3"/>
</ItemGroup>
@ -48,25 +47,7 @@
<EmbeddedResource Include="gitUnpushed.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="StaticFiles\css\themes\default\assets\fonts\outline-icons.woff2"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\outline-icons.woff"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\outline-icons.ttf"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\outline-icons.svg"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\outline-icons.eot"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\icons.woff2"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\icons.woff"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\icons.ttf"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\icons.svg"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\icons.eot"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\brand-icons.woff2"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\brand-icons.woff"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\brand-icons.ttf"/>
<None Remove="StaticFiles\css\themes\default\assets\fonts\brand-icons.svg"/>
<None Remove="StaticFiles\css\semantic.min.css"/>
</ItemGroup>
<ItemGroup>
<Folder Include="StaticFiles\css\themes\default\assets\fonts"/>
<None Remove="recent-activity.xml"/>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
@ -75,5 +56,4 @@
<Exec Command="git remote -v &gt; &quot;$(ProjectDir)/gitRemotes.txt&quot;"/>
<Exec Command="git log --branches --not --remotes --oneline &gt; &quot;$(ProjectDir)/gitUnpushed.txt&quot;"/>
</Target>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -12,6 +12,17 @@ div.main {
margin-bottom: 1px;
}
canvas.photo-subjects {
opacity: 1;
transition: opacity 0.3s;
pointer-events: none;
}
canvas.hide-subjects {
opacity: 0;
pointer-events: none;
}
/*#region Cards*/
.card {
@ -24,6 +35,7 @@ div.main {
background-size: cover;
background-position: center;
border-radius: 100%;
object-fit: cover;
}
.cardStats {

View file

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

View file

@ -8,6 +8,7 @@ public enum LbpFileType
FileArchive, // .farc, (ends with FARC)
Plan, // PLN, uploaded with levels
Voice, // VOP, voice data
MotionRecording, // used in LBP2+/V for the motion recorder
Painting, // PTG, paintings
Jpeg, // JFIF / FIF, used in sticker switches,
Png, // used in LBP Vita

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,58 @@ 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.";
}
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

@ -5,15 +5,17 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
#if RELEASE
using LBPUnion.ProjectLighthouse.Helpers;
#endif
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 = 20; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
private static FileSystemWatcher fileWatcher;
static ServerSettings()
{
@ -132,6 +134,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; } = "";
@ -150,6 +158,12 @@ public class ServerSettings
public string MissingIconHash { get; set; } = "";
public bool HCaptchaEnabled { get; set; }
public string HCaptchaSiteKey { get; set; } = "";
public string HCaptchaSecret { get; set; } = "";
#region Meta
[NotNull]

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