Merge main into mod-panel

This commit is contained in:
jvyden 2022-06-10 04:01:06 -04:00
commit b2e6f25265
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
38 changed files with 468 additions and 139 deletions

View file

@ -3,11 +3,13 @@
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes>
<Path>.config/dotnet-tools.json</Path>
<Path>.github</Path>
<Path>.gitignore</Path>
<Path>.idea</Path>
<Path>CONTRIBUTING.md</Path>
<Path>DatabaseMigrations</Path>
<Path>LICENSE</Path>
<Path>ProjectLighthouse.sln.DotSettings</Path>
<Path>ProjectLighthouse.sln.DotSettings.user</Path>
<Path>README.md</Path>

View file

@ -130,7 +130,7 @@ public class LoginController : ControllerBase
new LoginResult
{
AuthTicket = "MM_AUTH=" + token.UserToken,
LbpEnvVer = ServerStatics.ServerName,
ServerBrand = VersionHelper.FullVersion,
}.Serialize()
);
}

View file

@ -86,9 +86,9 @@ public class PublishController : ControllerBase
if (slot.Location == null) return this.BadRequest();
if (slot.Description.Length > 200) return this.BadRequest();
if (slot.Description.Length > 500) return this.BadRequest();
if (slot.Name.Length > 100) return this.BadRequest();
if (slot.Name.Length > 64) return this.BadRequest();
if (slot.Resources.Any(resource => !FileHelper.ResourceExists(resource)))
{

View file

@ -96,7 +96,7 @@ public class ReviewController : ControllerBase
Review? newReview = await this.getReviewFromBody();
if (newReview == null) return this.BadRequest();
if (newReview.Text.Length > 100) return this.BadRequest();
if (newReview.Text.Length > 512) return this.BadRequest();
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);

View file

@ -42,7 +42,7 @@ public class RoomVisualizerController : ControllerBase
#if !DEBUG
return this.NotFound();
#else
RoomHelper.Rooms.RemoveAll();
lock(RoomHelper.RoomLock) RoomHelper.Rooms.RemoveAll();
return this.Redirect("/debug/roomVisualizer");
#endif
}

View file

@ -1,14 +1,16 @@
@page "/"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@using LBPUnion.ProjectLighthouse.Levels
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LandingPage
@{
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
bool isMobile = this.Request.IsMobile();
}
<h1>Welcome to <b>Project Lighthouse</b>!</h1>
<h1>Welcome to <b>@ServerConfiguration.Instance.Customization.ServerName</b>!</h1>
@if (Model.User != null)
{
@ -41,4 +43,39 @@ else
{
<a href="/user/@user.UserId" title="@user.Status.ToString()">@user.Username</a>
}
}
}
<br>
<div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column">
<div class="ui pink segment">
<h1><i class="ribbon icon"></i>Latest Team Picks</h1>
<div class="ui divider"></div>
<div class="ui left aligned segment">
@foreach (Slot slot in Model.LatestTeamPicks!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
<br>
}
</div>
</div>
</div>
@if (isMobile)
{
<br>
}
<div class="eight wide column">
<div class="ui blue segment">
<h1><i class="certificate icon"></i>Newest Levels</h1>
<div class="ui divider"></div>
<div class="ui left aligned segment">
@foreach (Slot slot in Model.NewestLevels!) @* Can't reach a point where this is null *@
{
@await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile))
<br>
}
</div>
</div>
</div>
</div>

View file

@ -1,23 +1,27 @@
#nullable enable
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class LandingPage : BaseLayout
{
public LandingPage(Database database) : base(database)
{}
public int AuthenticationAttemptsCount;
public List<User> PlayersOnline = new();
public int PlayersOnlineCount;
public LandingPage(Database database) : base(database)
{}
public List<Slot>? LatestTeamPicks;
public List<Slot>? NewestLevels;
[UsedImplicitly]
public async Task<IActionResult> OnGet()
@ -35,6 +39,38 @@ public class LandingPage : BaseLayout
List<int> userIds = await this.Database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync();
const int maxShownLevels = 5;
this.LatestTeamPicks = await this.Database.Slots.Where
(s => s.TeamPick)
.OrderByDescending(s => s.FirstUploaded)
.Take(maxShownLevels)
.Include(s => s.Creator)
.ToListAsync();
this.NewestLevels = await this.Database.Slots.OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync();
return this.Page();
}
public ViewDataDictionary GetSlotViewData(int slotId, bool isMobile = false)
=> new(ViewData)
{
{
"User", this.User
},
{
"CallbackUrl", $"~/slot/{slotId}"
},
{
"ShowLink", true
},
{
"IsMini", true
},
{
"IsMobile", isMobile
},
};
}

View file

@ -33,11 +33,11 @@
<head>
@if (Model.Title == string.Empty)
{
<title>Project Lighthouse</title>
<title>@ServerConfiguration.Instance.Customization.ServerName</title>
}
else
{
<title>Project Lighthouse - @Model.Title</title>
<title>@ServerConfiguration.Instance.Customization.ServerName - @Model.Title</title>
}
<link rel="stylesheet" type="text/css" href="~/css/styles.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css">
@ -52,7 +52,7 @@
@* Embed Stuff *@
<meta name="theme-color" data-react-helmet="true" content="#008cff">
<meta content="Project Lighthouse - @Model.Title" property="og:title">
<meta content="@ServerConfiguration.Instance.Customization.ServerName - @Model.Title" property="og:title">
@if (!string.IsNullOrEmpty(Model.Description))
{
<meta content="@Model.Description" property="og:description">

View file

@ -2,7 +2,6 @@
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@using Microsoft.EntityFrameworkCore
@model LBPUnion.ProjectLighthouse.Levels.Slot
@ -12,8 +11,10 @@
await using Database database = new();
string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false;
bool isQueued = false;
bool isHearted = false;
@ -31,25 +32,44 @@
}
<div class="card">
@{
int size = isMobile ? 50 : 100;
int size = isMobile || mini ? 50 : 100;
}
<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)
@if (!mini)
{
<h2>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h2>
@if (showLink)
{
<h2>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h2>
}
else
{
<h1>
@slotName
</h1>
}
}
else
{
<h1>
@slotName
</h1>
@if (showLink)
{
<h3>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h3>
}
else
{
<h3>
@slotName
</h3>
}
}
<div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue play icon" title="Plays"></i> <span>@Model.PlaysUnique</span>
@ -68,7 +88,7 @@
</div>
<div class="cardButtons">
<br>
@if (user != null)
@if (user != null && !mini)
{
if (isHearted)
{

View file

@ -1,5 +1,6 @@
#nullable enable
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
@ -38,7 +39,8 @@ public class PasswordResetPage : BaseLayout
await this.Database.SaveChangesAsync();
if (!user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail");
if (!user.EmailAddressVerified && ServerConfiguration.Instance.Mail.MailEnabled)
return this.Redirect("~/login/sendVerificationEmail");
return this.Redirect("~/");
}

View file

@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Tests;
using Xunit;
@ -25,7 +26,7 @@ public class AuthenticationTests : LighthouseServerTest
Assert.True(response.IsSuccessStatusCode);
string responseContent = await response.Content.ReadAsStringAsync();
Assert.Contains("MM_AUTH=", responseContent);
Assert.Contains(ServerStatics.ServerName, responseContent);
Assert.Contains(VersionHelper.FullVersion, responseContent);
}
[DatabaseFact]
@ -35,10 +36,10 @@ public class AuthenticationTests : LighthouseServerTest
Assert.NotNull(loginResult);
Assert.NotNull(loginResult.AuthTicket);
Assert.NotNull(loginResult.LbpEnvVer);
Assert.NotNull(loginResult.ServerBrand);
Assert.Contains("MM_AUTH=", loginResult.AuthTicket);
Assert.Equal(ServerStatics.ServerName, loginResult.LbpEnvVer);
Assert.Equal(VersionHelper.FullVersion, loginResult.ServerBrand);
}
[DatabaseFact]

View file

@ -15,7 +15,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.2.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

View file

@ -115,6 +115,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=dpadrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ezoiar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=farc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=FLUSHALL/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=friendscores/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ingame/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kettu/@EntryIndexedValue">True</s:Boolean>

View file

@ -0,0 +1,25 @@
using System;
using System.ComponentModel.DataAnnotations;
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
namespace LBPUnion.ProjectLighthouse.Administration;
/// <summary>
/// A record of the completion of a <see cref="IMigrationTask"/>.
/// </summary>
public class CompletedMigration
{
/// <summary>
/// The name of the migration.
/// </summary>
/// <remarks>
/// Do not use the user-friendly name when setting this.
/// </remarks>
[Key]
public string MigrationName { get; set; }
/// <summary>
/// The moment the migration was ran.
/// </summary>
public DateTime RanAt { get; set; }
}

View file

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.StorableLists;
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands;
public class FlushRedisCommand : ICommand
{
public string Name() => "Flush Redis";
public string[] Aliases() => new[] {
"flush", "flush-redis",
};
public string Arguments() => "";
public int RequiredArgs() => 0;
public async Task Run(string[] args, Logger logger)
{
await RedisDatabase.FlushAll();
}
}

View file

@ -1,7 +1,9 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IMaintenanceJob
{
public Task Run();

View file

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public interface IMigrationTask
{
/// <summary>
/// The user-friendly name of a migration.
/// </summary>
public string Name();
/// <summary>
/// Performs the migration.
/// </summary>
/// <param name="database">The Lighthouse database.</param>
/// <returns>True if successful, false if not.</returns>
internal Task<bool> Run(Database database);
}

View file

@ -1,9 +1,11 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Logging.Loggers;
@ -11,24 +13,16 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance;
public static class MaintenanceHelper
{
static MaintenanceHelper()
{
Commands = getListOfInterfaceObjects<ICommand>();
MaintenanceJobs = getListOfInterfaceObjects<IMaintenanceJob>();
MigrationTasks = getListOfInterfaceObjects<IMigrationTask>();
}
public static List<ICommand> Commands { get; }
public static List<IMaintenanceJob> MaintenanceJobs { get; }
private static List<T> getListOfInterfaceObjects<T>() where T : class
{
return Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null)
.Select(t => Activator.CreateInstance(t) as T)
.ToList()!;
}
public static List<IMigrationTask> MigrationTasks { get; }
public static async Task<List<LogLine>> RunCommand(string[] args)
{
@ -66,11 +60,57 @@ public static class MaintenanceHelper
IMaintenanceJob? job = MaintenanceJobs.FirstOrDefault(j => j.GetType().Name == jobName);
if (job == null) throw new ArgumentNullException();
await RunMaintenanceJob(job);
}
public static async Task RunMaintenanceJob(IMaintenanceJob job)
{
await job.Run();
}
public static async Task RunMigration(IMigrationTask migrationTask, Database? database = null)
{
database ??= new Database();
// Migrations should never be run twice.
Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name));
Logger.Info($"Running migration task {migrationTask.Name()}", LogArea.Database);
bool success;
Exception? exception = null;
try
{
success = await migrationTask.Run(database);
}
catch(Exception e)
{
success = false;
exception = e;
}
if(!success)
{
Logger.Error($"Could not run migration {migrationTask.Name()}", LogArea.Database);
if (exception != null) Logger.Error(exception.ToDetailedException(), LogArea.Database);
return;
}
Logger.Success($"Successfully completed migration {migrationTask.Name()}", LogArea.Database);
CompletedMigration completedMigration = new()
{
MigrationName = migrationTask.GetType().Name,
RanAt = DateTime.Now,
};
database.CompletedMigrations.Add(completedMigration);
await database.SaveChangesAsync();
}
private static List<T> getListOfInterfaceObjects<T>() where T : class
{
return Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null)
.Select(t => Activator.CreateInstance(t) as T)
.ToList()!;
}
}

View file

@ -25,7 +25,7 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob
bool largeHashIsInvalidFile = false;
bool tooManyPhotoSubjects = false;
bool duplicatePhotoSubjects = false;
bool takenInTheFuture = true;
bool takenInTheFuture = false;
// Checks should generally be ordered in least computationally expensive to most.

View file

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

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MigrationTasks;
public class CleanupXmlInjectionMigration : IMigrationTask
{
public string Name() => "Cleanup XML injections";
// Weird, but required. Thanks, hejlsberg.
async Task<bool> IMigrationTask.Run(Database database)
{
List<object> objsToBeSanitized = new();
// Store all the objects we need to sanitize in a list.
// The alternative here is to loop through every table, but thats a ton of code...
objsToBeSanitized.AddRange(database.Slots);
objsToBeSanitized.AddRange(database.Reviews);
objsToBeSanitized.AddRange(database.Comments);
objsToBeSanitized.AddRange(database.Scores);
objsToBeSanitized.AddRange(database.Users);
objsToBeSanitized.AddRange(database.Photos);
objsToBeSanitized.AddRange(database.Reports);
foreach (object obj in objsToBeSanitized) SanitizationHelper.SanitizeStringsInClass(obj);
await database.SaveChangesAsync();
return true;
}
}

View file

@ -0,0 +1,6 @@
namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
public class CustomizationConfiguration
{
public string ServerName { get; set; } = "Project Lighthouse";
}

View file

@ -23,7 +23,7 @@ public class ServerConfiguration
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
//
// Thanks for listening~
public const int CurrentConfigVersion = 4;
public const int CurrentConfigVersion = 5;
#region Meta
@ -163,6 +163,9 @@ public class ServerConfiguration
#endregion
// TODO: Find a way to properly remove config options
// YamlDotNet hates that and it's fucking annoying.
// This seriously sucks. /rant
[Obsolete("Obsolete. Use the Website/GameApi/Api listen URLS instead.")]
public string ListenUrl { get; set; } = "http://localhost:10060";
@ -193,5 +196,5 @@ public class ServerConfiguration
public MailConfiguration Mail { get; set; } = new();
public UserGeneratedContentLimitConfiguration UserGeneratedContentLimits { get; set; } = new();
public WebsiteConfiguration WebsiteConfiguration { get; set; } = new();
public CustomizationConfiguration Customization { get; set; } = new();
}

View file

@ -2,13 +2,12 @@
using System;
using System.Linq;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types;
namespace LBPUnion.ProjectLighthouse.Configuration;
public static class ServerStatics
{
public const string ServerName = "ProjectLighthouse";
public const int PageSize = 20;
public static bool DbConnected {
@ -25,11 +24,18 @@ public static class ServerStatics
}
}
// FIXME: This needs to go at some point.
public static bool IsUnitTesting => AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName!.StartsWith("xunit"));
#if DEBUG
public static readonly bool IsDebug = true;
public const bool IsDebug = true;
#else
public static readonly bool IsDebug = false;
public const bool IsDebug = false;
#endif
/// <summary>
/// The servertype, determined on startup. Shouldn't be null unless very very early in startup.
/// </summary>
// The way of doing this is kinda weird, but it works.
public static ServerType ServerType;
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Administration.Reports;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
@ -21,6 +22,7 @@ namespace LBPUnion.ProjectLighthouse;
public class Database : DbContext
{
public DbSet<CompletedMigration> CompletedMigrations { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Location> Locations { get; set; }
public DbSet<Slot> Slots { get; set; }

View file

@ -1,4 +1,7 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Reviews;
@ -52,4 +55,7 @@ public static class DatabaseExtensions
return query;
}
public static async Task<bool> Has<T>(this IQueryable<T> queryable, Expression<Func<T, bool>> predicate) =>
await queryable.FirstOrDefaultAsync(predicate) != null;
}

View file

@ -31,8 +31,8 @@ public static class VersionHelper
{
Logger.Error
(
"Project Lighthouse was built incorrectly. Please make sure git is available when building. " +
"Because of this, you will not be notified of updates.",
"Project Lighthouse was built incorrectly. Please make sure git is available when building.",
// "Because of this, you will not be notified of updates.",
LogArea.Startup
);
CommitHash = "invalid";
@ -54,14 +54,14 @@ public static class VersionHelper
public static string CommitHash { get; set; }
public static string Branch { get; set; }
public static string FullVersion => $"{ServerStatics.ServerName} {Branch}@{CommitHash} {Build}";
public static string FullVersion => $"Project Lighthouse {Branch}@{CommitHash} {Build} ({ServerConfiguration.Instance.Customization.ServerName})";
public static bool IsDirty => CommitHash.EndsWith("-dirty") || CommitsOutOfDate != 1 || CommitHash == "invalid" || Branch == "invalid";
public static int CommitsOutOfDate { get; set; }
public static bool CanCheckForUpdates { get; set; }
public static string[] Remotes { get; set; }
public const string Build =
#if DEBUG
#if DEBUG
"Debug";
#elif RELEASE
"Release";

View file

@ -11,7 +11,8 @@ public class ConsoleLogger : ILogger
foreach (string line in logLine.Message.Split('\n'))
{
// The following is scuffed. Beware~
// The following is scuffed.
// Beware~
// Write the level! [Success]
Console.ForegroundColor = ConsoleColor.White;

View file

@ -1,11 +1,11 @@
using System;
using System.IO;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Logging.Loggers;
public class LighthouseFileLogger : ILogger
public class FileLogger : ILogger
{
private static readonly string logsDirectory = Path.Combine(Environment.CurrentDirectory, "logs");
@ -13,8 +13,8 @@ public class LighthouseFileLogger : ILogger
{
FileHelper.EnsureDirectoryCreated(logsDirectory);
string contentFile = $"[{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n";
string contentAll = $"[{line.Area}:{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n";
string contentFile = $"[{ServerStatics.ServerType}] [{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n";
string contentAll = $"[{ServerStatics.ServerType}] [{line.Area}:{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n";
try
{

View file

@ -16,7 +16,8 @@ namespace LBPUnion.ProjectLighthouse.Match.Rooms;
public class RoomHelper
{
public static readonly StorableList<Room> Rooms = RoomStore.GetRooms();
public static readonly object RoomLock = new();
public static StorableList<Room> Rooms => RoomStore.GetRooms();
public static void StartCleanupThread()
{
@ -162,7 +163,7 @@ public class RoomHelper
};
CleanupRooms(room.HostId, room);
lock(Rooms) Rooms.Add(room);
lock(RoomLock) Rooms.Add(room);
Logger.Info($"Created room (id: {room.RoomId}) for host {room.HostId}", LogArea.Match);
return room;
@ -193,7 +194,7 @@ public class RoomHelper
[SuppressMessage("ReSharper", "InvertIf")]
public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null)
{
lock(Rooms)
lock(RoomLock)
{
int roomCountBeforeCleanup = Rooms.Count();

View file

@ -0,0 +1,37 @@
using System;
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220610061641_AddCompletedMigrations")]
public class AddCompletedMigrations : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CompletedMigrations",
columns: table => new
{
MigrationName = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
RanAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CompletedMigrations", x => x.MigrationName);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CompletedMigrations");
}
}
}

View file

@ -1,4 +1,5 @@
// <auto-generated />
using System;
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -18,6 +19,19 @@ namespace ProjectLighthouse.Migrations
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b =>
{
b.Property<string>("MigrationName")
.HasColumnType("varchar(255)");
b.Property<DateTime>("RanAt")
.HasColumnType("datetime(6)");
b.HasKey("MigrationName");
b.ToTable("CompletedMigrations");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.Reports.GriefReport", b =>
{
b.Property<int>("ReportId")
@ -581,15 +595,16 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("AdminGrantedSlots")
.HasColumnType("int");
b.Property<bool>("Banned")
.HasColumnType("tinyint(1)");
b.Property<string>("BannedReason")
.HasColumnType("longtext");
b.Property<string>("Biography")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("BooHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("EmailAddress")
@ -602,48 +617,39 @@ namespace ProjectLighthouse.Migrations
.HasColumnType("int");
b.Property<string>("IconHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsAdmin")
.HasColumnType("tinyint(1)");
b.Property<int>("LocationId")
.HasColumnType("int");
b.Property<string>("MehHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("PasswordResetRequired")
.HasColumnType("tinyint(1)");
b.Property<int>("PermissionLevel")
.HasColumnType("int");
b.Property<string>("Pins")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PlanetHashLBP2")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PlanetHashLBP3")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PlanetHashLBPVita")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("YayHash")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("UserId");

View file

@ -15,9 +15,12 @@ public class LoginResult
public string AuthTicket { get; set; }
[XmlElement("lbpEnvVer")]
public string LbpEnvVer { get; set; }
public string ServerBrand { get; set; }
public string Serialize()
=> LbpSerializer.Elements
(new KeyValuePair<string, object>("authTicket", this.AuthTicket), new KeyValuePair<string, object>("lbpEnvVer", this.LbpEnvVer));
(
new KeyValuePair<string, object>("authTicket", this.AuthTicket),
new KeyValuePair<string, object>("lbpEnvVer", this.ServerBrand)
);
}

View file

@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="DDSReader" Version="1.0.8-pre" />
<PackageReference Include="Discord.Net.Webhook" Version="3.6.1" />
<PackageReference Include="Discord.Net.Webhook" Version="3.7.2" />
<PackageReference Include="InfluxDB.Client" Version="4.2.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.5" />
@ -50,10 +50,6 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Remove="Migrations\20220522192158_SwitchToPermissionLevels.Designer.cs" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="git describe --long --always --dirty --exclude=\* --abbrev=8 &gt; &quot;$(ProjectDir)/gitVersion.txt&quot;" />
<Exec Command="git branch --show-current &gt; &quot;$(ProjectDir)/gitBranch.txt&quot;" />

View file

@ -20,7 +20,6 @@ public class DebugWarmupLifetime : IHostLifetime
private CancellationTokenRegistration applicationStartedRegistration;
private readonly ConsoleLifetime consoleLifetime;
public static ServerType ServerType;
public DebugWarmupLifetime
(
@ -39,7 +38,7 @@ public class DebugWarmupLifetime : IHostLifetime
{
using HttpClient client = new();
string url = ServerType switch
string url = ServerStatics.ServerType switch
{
ServerType.GameServer => ServerConfiguration.Instance.GameApiListenUrl,
ServerType.Website => ServerConfiguration.Instance.WebsiteListenUrl,

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
@ -9,6 +11,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Logging.Loggers;
using LBPUnion.ProjectLighthouse.Match.Rooms;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Startup;
using LBPUnion.ProjectLighthouse.StorableLists;
using LBPUnion.ProjectLighthouse.Types;
@ -24,18 +27,16 @@ public static class StartupTasks
Stopwatch stopwatch = new();
stopwatch.Start();
#if DEBUG
DebugWarmupLifetime.ServerType = serverType;
#endif
ServerStatics.ServerType = serverType;
// Setup logging
Logger.Instance.AddLogger(new ConsoleLogger());
Logger.Instance.AddLogger(new LighthouseFileLogger());
Logger.Instance.AddLogger(new FileLogger());
Logger.Info($"Welcome to the Project Lighthouse {serverType.ToString()}!", LogArea.Startup);
Logger.Info($"You are running version {VersionHelper.FullVersion}", LogArea.Startup);
// Referencing ServerSettings.Instance here loads the config, see ServerSettings.cs for more information
// Referencing ServerConfiguration.Instance here loads the config, see ServerConfiguration.cs for more information
Logger.Success("Loaded config file version " + ServerConfiguration.Instance.ConfigVersion, LogArea.Startup);
Logger.Info("Connecting to the database...", LogArea.Startup);
@ -46,13 +47,15 @@ public static class StartupTasks
}
else
{
Logger.Success("Connected!", LogArea.Startup);
Logger.Success("Connected to the database!", LogArea.Startup);
}
if (!dbConnected) Environment.Exit(1);
using Database database = new();
Logger.Info("Migrating database...", LogArea.Database);
#if !DEBUG
if(serverType == ServerType.GameServer)
#endif
migrateDatabase(database);
if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled)
@ -83,25 +86,66 @@ public static class StartupTasks
{
FileHelper.ConvertAllTexturesToPng();
}
Logger.Info("Starting room cleanup thread...", LogArea.Startup);
RoomHelper.StartCleanupThread();
Logger.Info("Initializing Redis...", LogArea.Startup);
RedisDatabase.Initialize().Wait();
if (serverType == ServerType.GameServer)
{
Logger.Info("Starting room cleanup thread...", LogArea.Startup);
RoomHelper.StartCleanupThread();
}
// Create admin user if no users exist
if (serverType == ServerType.Website && database.Users.CountAsync().Result == 0)
{
const string passwordClear = "lighthouse";
string password = CryptoHelper.BCryptHash(CryptoHelper.Sha256Hash(passwordClear));
User admin = database.CreateUser("admin", password).Result;
admin.IsAdmin = true;
admin.PasswordResetRequired = true;
database.SaveChanges();
Logger.Success("No users were found, so an admin user was created. " +
$"The username is 'admin' and the password is '{passwordClear}'.", LogArea.Startup);
}
stopwatch.Stop();
Logger.Success($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup);
}
private static void migrateDatabase(Database database)
{
Logger.Info("Migrating database...", LogArea.Database);
Stopwatch totalStopwatch = new();
Stopwatch stopwatch = new();
totalStopwatch.Start();
stopwatch.Start();
database.Database.MigrateAsync().Wait();
stopwatch.Stop();
Logger.Success($"Structure migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
stopwatch.Reset();
stopwatch.Start();
List<CompletedMigration> completedMigrations = database.CompletedMigrations.ToList();
List<IMigrationTask> migrationsToRun = MaintenanceHelper.MigrationTasks
.Where(migrationTask => !completedMigrations
.Select(m => m.MigrationName)
.Contains(migrationTask.GetType().Name)
).ToList();
foreach (IMigrationTask migrationTask in migrationsToRun)
{
MaintenanceHelper.RunMigration(migrationTask, database).Wait();
}
stopwatch.Stop();
Logger.Success($"Migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
totalStopwatch.Stop();
Logger.Success($"Extra migration tasks took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
Logger.Success($"Total migration took {totalStopwatch.ElapsedMilliseconds}ms.", LogArea.Database);
}
}

View file

@ -39,8 +39,7 @@ public static class RedisDatabase
return;
}
await connection.RecreateIndexAsync(typeof(Room));
await connection.RecreateIndexAsync(typeof(UserFriendData));
await createIndexes(connection);
}
catch(Exception e)
{
@ -52,6 +51,20 @@ public static class RedisDatabase
Logger.Success("Initialized Redis.", LogArea.Redis);
}
public static async Task FlushAll()
{
IRedisConnection connection = getConnection();
await connection.ExecuteAsync("FLUSHALL");
await createIndexes(connection);
}
private static async Task createIndexes(IRedisConnection connection)
{
await connection.RecreateIndexAsync(typeof(Room));
await connection.RecreateIndexAsync(typeof(UserFriendData));
}
private static IRedisConnection getConnection()
{
Logger.Debug("Getting a Redis connection", LogArea.Redis);

View file

@ -1,4 +1,9 @@
#!/bin/bash
# Developer script to create EntityFramework database migrations
#
# $1: Name of the migration, e.g. SwitchToPermissionLevels
# Invoked manually
export LIGHTHOUSE_DB_CONNECTION_STRING='server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse'
dotnet ef migrations add "$1" --project ../ProjectLighthouse