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

This commit is contained in:
FridiNaTor1 2022-02-03 08:02:37 +00:00
commit d638357f34
76 changed files with 1102 additions and 417 deletions

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ gitVersion.txt
gitRemotes.txt gitRemotes.txt
gitUnpushed.txt gitUnpushed.txt
logs/* logs/*
npTicket*
# MSBuild stuff # MSBuild stuff
bin/ bin/

View file

@ -6,6 +6,7 @@
<Path>.github</Path> <Path>.github</Path>
<Path>.gitignore</Path> <Path>.gitignore</Path>
<Path>.idea</Path> <Path>.idea</Path>
<Path>CONTRIBUTING.md</Path>
<Path>DatabaseMigrations</Path> <Path>DatabaseMigrations</Path>
<Path>ProjectLighthouse.sln.DotSettings</Path> <Path>ProjectLighthouse.sln.DotSettings</Path>
<Path>ProjectLighthouse.sln.DotSettings.user</Path> <Path>ProjectLighthouse.sln.DotSettings.user</Path>

100
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,100 @@
# Contributing
## Setting up MySQL
Project Lighthouse requires a MySQL database. For Linux users running docker, one can be set up using
the `docker-compose.yml` file in the root of the project folder.
Next, make sure the `LIGHTHOUSE_DB_CONNECTION_STRING` environment variable is set correctly. By default, it
is `server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse`. If you are running the database via the
above `docker-compose.yml` you shouldn't need to change this. For other development/especially production environments
you will need to change this.
Once you've gotten MySQL running you can run Lighthouse. It will take care of the rest.
## Connecting
PS3 is difficult to set up, so I will be going over how to set up RPCS3 instead. A guide will be coming for PS3 closer
to release. You can also follow this guide if you want to learn how to modify your EBOOT.
*Note: This requires a patched copy of RPCS3. You can find a working
version [on our GitHub](https://github.com/LBPUnion/rpcs3).*
Start by getting a copy of LittleBigPlanet 1/2 installed. (Check the LittleBigPlanet 1 section, since you'll need to do
extra steps for your game to not crash upon entering pod computer).
The game can be a digital copy (NPUA80472/NPUA80662) or a disc copy (
BCUS98148/BCUS98245).
Next, download [UnionPatcher](https://github.com/LBPUnion/UnionPatcher/). Binaries can be found by reading the `README.md`
file.
You should have everything you need now, so open up RPCS3 and go to Utilities -> Decrypt PS3 Binaries. Point this
to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`. You can grab your title id by right clicking the game in RPCS3 and
clicking Copy Info -> Copy Serial.
This should give you a file named `EBOOT.elf` in the same folder. This is your decrypted eboot.
Now that you have your decrypted eboot, open UnionPatcher and select the `EBOOT.elf` you got earlier in the top box,
enter `http://localhost:10060/LITTLEBIGPLANETPS3_XML` in the second, and the output filename in the third. For this
guide I'll use `EBOOTlocalhost.elf`.
Now, copy the `EBOOTlocalhost.elf` file to where you got your `EBOOT.elf` file from, and you're now good to go.
To launch the game with the patched EBOOT, open up RPCS3, go to File, Boot SELF/ELF, and open up `EBOOTlocalhost.elf`.
Assuming you are running the patched version of RPCS3, you patched the file correctly, the database is migrated, and
Project Lighthouse is running, the game should now connect and you may begin contributing!
### LittleBigPlanet 1
For LittleBigPlanet 1 to work with RPCS3, follow the steps above normally.
First, open your favourite hex editor. We recommend [HxD](https://mh-nexus.de/en/hxd/).
Once you have a hex editor open, open your `EBOOTlocalhost.elf` file and search for the hex
values `73 63 65 4E 70 43 6F 6D 6D 65 72 63 65 32`. In HxD, this would be done by clicking on Search -> Replace,
clicking on the `Hex-values` tab, and entering the hex there.
Then, you can zero it out by replacing it with `00 00 00 00 00 00 00 00 00 00 00 00 00 00`.
What this does is remove all the references to the `sceNpCommerce2` function. The function is used for purchasing DLC,
which at this moment in time crashes RPCS3.
After saving the file your LBP1 EBOOT can be used with RPCS3.
## Contributing Tips
### Database migrations
Some modifications may require updates to the database schema. You can automatically create a migration file by:
1. Making sure the tools are installed. You can do this by running `dotnet tool restore`.
2. Making sure `LIGHTHOUSE_DB_CONNECTION_STRING` is set correctly. See the `Running` section for more details.
3. Modifying the database schema via the C# portion of the code. Do not modify the actual SQL database.
4. Running `dotnet ef migrations add <NameOfMigrationInPascalCase> --project ProjectLighthouse`.
This process will create a migration file from the changes made in the C# code.
The new migrations will automatically be applied upon starting Lighthouse.
### Running tests
You can run tests either through your IDE or by running `dotnet tests`.
Keep in mind while running database tests (which most tests are) you need to have `LIGHTHOUSE_DB_CONNECTION_STRING` set.
### Continuous Integration (CI) Tips
- You can skip CI runs for a commit if you specify `[skip ci]` at the beginning of the commit name. This is useful for
formatting changes, etc.
- When creating your first pull request, CI will not run initially. A team member will have to approve you for use of
running CI on a pull request. This is because of GitHub policy.
### API Documentation
You can access API documentation by looking at the XMLDoc in the controllers under `ProjectLighthouse.Controllers.Api`
You can also access an interactive version by starting Lighthouse and accessing Swagger
at `http://localhost:10060/swagger/index.html`.

View file

@ -37,7 +37,8 @@ public class LighthouseServerTest
await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}")); await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}"));
} }
string stringContent = $"{LoginData.UsernamePrefix}{username}{number}{(char)0x00}"; //TODO: generate actual tickets
string stringContent = $"unitTestTicket{username}{number}";
HttpResponseMessage response = await this.Client.PostAsync HttpResponseMessage response = await this.Client.PostAsync
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent)); ($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent));

View file

@ -1,5 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/Asp/FormatOnClosingTag/@EntryValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeEditing/TypingAssist/Asp/FormatOnClosingTag/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/CSharpAnnotationTypingAssist/IsEnabledAfterTypeName/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/CSharpAnnotationTypingAssist/IsEnabledAtOtherPositions/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/CSharpAnnotationTypingAssist/IsInsertNullCheckWhenAlreadyAnnotated/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeEditing/TypingAssist/FormatOnPaste/@EntryValue">FullFormat</s:String> <s:String x:Key="/Default/CodeEditing/TypingAssist/FormatOnPaste/@EntryValue">FullFormat</s:String>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/VirtualSpaceOnEnter/@EntryValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeEditing/TypingAssist/VirtualSpaceOnEnter/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeConstructorOrDestructorBody/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeConstructorOrDestructorBody/@EntryIndexedValue">HINT</s:String>

View file

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers.Api;
/// <summary>
/// A collection of endpoints relating to slots.
/// </summary>
public class SlotEndpoints : ApiEndpointController
{
private readonly Database database;
public SlotEndpoints(Database database)
{
this.database = database;
}
/// <summary>
/// Gets a list of (stripped down) slots from the database.
/// </summary>
/// <param name="limit">How many slots you want to retrieve.</param>
/// <param name="skip">How many slots to skip.</param>
/// <returns>The slot</returns>
/// <response code="200">The slot list, if successful.</response>
[HttpGet("slots")]
[ProducesResponseType(typeof(List<MinimalSlot>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetSlots([FromQuery] int limit = 20, [FromQuery] int skip = 0)
{
limit = Math.Min(ServerStatics.PageSize, limit);
IEnumerable<MinimalSlot> minimalSlots = (await this.database.Slots.OrderByDescending(s => s.FirstUploaded).Skip(skip).Take(limit).ToListAsync()).Select
(MinimalSlot.FromSlot);
return this.Ok(minimalSlots);
}
/// <summary>
/// Gets a slot (more commonly known as a level) and its information from the database.
/// </summary>
/// <param name="id">The ID of the slot</param>
/// <returns>The slot</returns>
/// <response code="200">The slot, if successful.</response>
/// <response code="404">The slot could not be found.</response>
[HttpGet("slot/{id:int}")]
[ProducesResponseType(typeof(Slot), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetSlot(int id)
{
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(u => u.SlotId == id);
if (slot == null) return this.NotFound();
return this.Ok(slot);
}
}

View file

@ -0,0 +1,33 @@
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Api;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers.Api;
/// <summary>
/// A collection of endpoints relating to statistics.
/// </summary>
public class StatisticsEndpoints : ApiEndpointController
{
/// <summary>
/// Gets everything that StatisticsHelper provides.
/// </summary>
/// <returns>An instance of StatisticsResponse</returns>
[HttpGet("statistics")]
[ProducesResponseType(typeof(StatisticsResponse), StatusCodes.Status200OK)]
public async Task<IActionResult> GetStatistics()
=> this.Ok
(
new StatisticsResponse
{
Photos = await StatisticsHelper.PhotoCount(),
Slots = await StatisticsHelper.SlotCount(),
Users = await StatisticsHelper.UserCount(),
RecentMatches = await StatisticsHelper.RecentMatches(),
TeamPicks = await StatisticsHelper.TeamPickCount(),
}
);
}

View file

@ -0,0 +1,39 @@
#nullable enable
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers.Api;
/// <summary>
/// A collection of endpoints relating to users.
/// </summary>
public class UserEndpoints : ApiEndpointController
{
private readonly Database database;
public UserEndpoints(Database database)
{
this.database = database;
}
/// <summary>
/// Gets a user and their information from the database.
/// </summary>
/// <param name="id">The ID of the user</param>
/// <returns>The user</returns>
/// <response code="200">The user, if successful.</response>
/// <response code="404">The user could not be found.</response>
[HttpGet("user/{id:int}")]
[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetUser(int id)
{
User? user = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (user == null) return this.NotFound();
return this.Ok(user);
}
}

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -8,10 +8,12 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
using LBPUnion.ProjectLighthouse.Types.Tickets;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using IOFile = System.IO.File;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/login")] [Route("LITTLEBIGPLANETPS3_XML/login")]
@ -26,25 +28,29 @@ public class LoginController : ControllerBase
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> Login([FromQuery] string? titleId) public async Task<IActionResult> Login()
{ {
titleId ??= ""; MemoryStream ms = new();
await this.Request.Body.CopyToAsync(ms);
byte[] loginData = ms.ToArray();
string body = await new StreamReader(this.Request.Body).ReadToEndAsync(); #if DEBUG
await IOFile.WriteAllBytesAsync($"npTicket-{TimestampHelper.TimestampMillis}.txt", loginData);
#endif
LoginData? loginData; NPTicket? npTicket;
try try
{ {
loginData = LoginData.CreateFromString(body); npTicket = NPTicket.CreateFromBytes(loginData);
} }
catch catch
{ {
loginData = null; npTicket = null;
} }
if (loginData == null) if (npTicket == null)
{ {
Logger.Log("loginData was null, rejecting login", LoggerLevelLogin.Instance); Logger.Log("npTicket was null, rejecting login", LoggerLevelLogin.Instance);
return this.BadRequest(); return this.BadRequest();
} }
@ -60,11 +66,11 @@ public class LoginController : ControllerBase
// Get an existing token from the IP & username // Get an existing token from the IP & username
GameToken? token = await this.database.GameTokens.Include GameToken? token = await this.database.GameTokens.Include
(t => t.User) (t => t.User)
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == loginData.Username && !t.Used); .FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && !t.Used);
if (token == null) // If we cant find an existing token, try to generate a new one if (token == null) // If we cant find an existing token, try to generate a new one
{ {
token = await this.database.AuthenticateUser(loginData, ipAddress, titleId); token = await this.database.AuthenticateUser(npTicket, ipAddress);
if (token == null) if (token == null)
{ {
Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance); Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance);
@ -110,7 +116,7 @@ public class LoginController : ControllerBase
GameTokenId = token.TokenId, GameTokenId = token.TokenId,
Timestamp = TimestampHelper.Timestamp, Timestamp = TimestampHelper.Timestamp,
IPAddress = ipAddress, IPAddress = ipAddress,
Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3 Platform = npTicket.Platform,
}; };
this.database.AuthenticationAttempts.Add(authAttempt); this.database.AuthenticationAttempts.Add(authAttempt);
@ -129,7 +135,7 @@ public class LoginController : ControllerBase
return this.StatusCode(403, ""); return this.StatusCode(403, "");
} }
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance); Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client", LoggerLevelLogin.Instance);
// After this point we are now considering this session as logged in. // After this point we are now considering this session as logged in.
// We just logged in with the token. Mark it as used so someone else doesnt try to use it, // We just logged in with the token. Mark it as used so someone else doesnt try to use it,

View file

@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Matching;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -14,7 +14,7 @@ using LBPUnion.ProjectLighthouse.Types.Match;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Matching;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -15,7 +15,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Resources;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -1,4 +1,5 @@
#nullable enable #nullable enable
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Files;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using IOFile = System.IO.File; using IOFile = System.IO.File;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Resources;
[ApiController] [ApiController]
[Produces("text/xml")] [Produces("text/xml")]

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Categories;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -2,7 +2,7 @@ using System;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/tags")] [Route("LITTLEBIGPLANETPS3_XML/tags")]

View file

@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -38,7 +38,7 @@ public class PublishController : ControllerBase
if (user.UsedSlots >= ServerSettings.Instance.EntitledSlots) return this.BadRequest(); if (user.UsedSlots >= ServerSettings.Instance.EntitledSlots) return this.BadRequest();
Slot? slot = await this.GetSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot == null) return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded if (slot == null) return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded
if (string.IsNullOrEmpty(slot.RootLevel)) return this.BadRequest(); if (string.IsNullOrEmpty(slot.RootLevel)) return this.BadRequest();
@ -79,7 +79,7 @@ public class PublishController : ControllerBase
if (user.UsedSlots >= ServerSettings.Instance.EntitledSlots) return this.BadRequest(); if (user.UsedSlots >= ServerSettings.Instance.EntitledSlots) return this.BadRequest();
Slot? slot = await this.GetSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot?.Location == null) return this.BadRequest(); if (slot?.Location == null) return this.BadRequest();
// Republish logic // Republish logic
@ -186,7 +186,7 @@ public class PublishController : ControllerBase
return this.Ok(); return this.Ok();
} }
public async Task<Slot?> GetSlotFromBody() private async Task<Slot?> getSlotFromBody()
{ {
this.Request.Body.Position = 0; this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();

View file

@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Reviews;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -91,7 +91,7 @@ public class ReviewController : ControllerBase
if (user == null) return this.StatusCode(403, ""); if (user == null) return this.StatusCode(403, "");
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId); Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
Review? newReview = await this.GetReviewFromBody(); Review? newReview = await this.getReviewFromBody();
if (newReview == null) return this.BadRequest(); if (newReview == null) return this.BadRequest();
if (review == null) if (review == null)
@ -317,7 +317,7 @@ public class ReviewController : ControllerBase
return this.Ok(); return this.Ok();
} }
public async Task<Review?> GetReviewFromBody() private async Task<Review?> getReviewFromBody()
{ {
this.Request.Body.Position = 0; this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -80,7 +80,7 @@ public class ScoreController : ControllerBase
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
string myRanking = this.GetScores(score.SlotId, score.Type, user); string myRanking = this.getScores(score.SlotId, score.Type, user);
return this.Ok(myRanking); return this.Ok(myRanking);
} }
@ -99,11 +99,11 @@ public class ScoreController : ControllerBase
if (user == null) return this.StatusCode(403, ""); if (user == null) return this.StatusCode(403, "");
return this.Ok(this.GetScores(slotId, type, user, pageStart, pageSize)); return this.Ok(this.getScores(slotId, type, user, pageStart, pageSize));
} }
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public string GetScores(int slotId, int type, User user, int pageStart = -1, int pageSize = 5) private string getScores(int slotId, int type, User user, int pageStart = -1, int pageSize = 5)
{ {
// This is hella ugly but it technically assigns the proper rank to a score // This is hella ugly but it technically assigns the proper rank to a score
// var needed for Anonymous type returned from SELECT // var needed for Anonymous type returned from SELECT

View file

@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -177,7 +177,7 @@ public class SlotsController : ControllerBase
"hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots) "hint_start", pageStart + Math.Min(pageSize, ServerSettings.Instance.EntitledSlots)
}, },
{ {
"total", await StatisticsHelper.MMPicksCount() "total", await StatisticsHelper.TeamPickCount()
}, },
} }
) )
@ -235,7 +235,7 @@ public class SlotsController : ControllerBase
Random rand = new(); Random rand = new();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion) IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable() .AsEnumerable()
.OrderByDescending(s => s.Thumbsup) .OrderByDescending(s => s.Thumbsup)
.ThenBy(_ => rand.Next()) .ThenBy(_ => rand.Next())
@ -279,14 +279,14 @@ public class SlotsController : ControllerBase
Random rand = new(); Random rand = new();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion) IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable() .AsEnumerable()
.OrderByDescending .OrderByDescending
( (
// probably not the best way to do this? // probably not the best way to do this?
s => s =>
{ {
return this.GetGameFilter(gameFilterType, token.GameVersion) switch return this.getGameFilter(gameFilterType, token.GameVersion) switch
{ {
GameVersion.LittleBigPlanet1 => s.PlaysLBP1Unique, GameVersion.LittleBigPlanet1 => s.PlaysLBP1Unique,
GameVersion.LittleBigPlanet2 => s.PlaysLBP2Unique, GameVersion.LittleBigPlanet2 => s.PlaysLBP2Unique,
@ -337,7 +337,7 @@ public class SlotsController : ControllerBase
Random rand = new(); Random rand = new();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion) IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable() .AsEnumerable()
.OrderByDescending(s => s.Hearts) .OrderByDescending(s => s.Hearts)
.ThenBy(_ => rand.Next()) .ThenBy(_ => rand.Next())
@ -365,7 +365,7 @@ public class SlotsController : ControllerBase
); );
} }
public GameVersion GetGameFilter(string? gameFilterType, GameVersion version) private GameVersion getGameFilter(string? gameFilterType, GameVersion version)
{ {
if (version == GameVersion.LittleBigPlanetVita) return GameVersion.LittleBigPlanetVita; if (version == GameVersion.LittleBigPlanetVita) return GameVersion.LittleBigPlanetVita;
if (version == GameVersion.LittleBigPlanetPSP) return GameVersion.LittleBigPlanetPSP; if (version == GameVersion.LittleBigPlanetPSP) return GameVersion.LittleBigPlanetPSP;
@ -381,7 +381,7 @@ public class SlotsController : ControllerBase
}; };
} }
public IQueryable<Slot> FilterByRequest(string? gameFilterType, string? dateFilterType, GameVersion version) private IQueryable<Slot> filterByRequest(string? gameFilterType, string? dateFilterType, GameVersion version)
{ {
string _dateFilterType = dateFilterType ?? ""; string _dateFilterType = dateFilterType ?? "";
@ -392,7 +392,7 @@ public class SlotsController : ControllerBase
_ => 0, _ => 0,
}; };
GameVersion gameVersion = this.GetGameFilter(gameFilterType, version); GameVersion gameVersion = this.getGameFilter(gameFilterType, version);
IQueryable<Slot> whereSlots; IQueryable<Slot> whereSlots;

View file

@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -24,7 +24,7 @@ public class StatisticsController : ControllerBase
public async Task<IActionResult> PlanetStats() public async Task<IActionResult> PlanetStats()
{ {
int totalSlotCount = await StatisticsHelper.SlotCount(); int totalSlotCount = await StatisticsHelper.SlotCount();
int mmPicksCount = await StatisticsHelper.MMPicksCount(); int mmPicksCount = await StatisticsHelper.TeamPickCount();
return this.Ok return this.Ok
( (

View file

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers; namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController] [ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")] [Route("LITTLEBIGPLANETPS3_XML/")]
@ -26,7 +26,7 @@ public class UserController : ControllerBase
this.database = database; this.database = database;
} }
public async Task<string?> GetSerializedUser(string username, GameVersion gameVersion = GameVersion.LittleBigPlanet1) private async Task<string?> getSerializedUser(string username, GameVersion gameVersion = GameVersion.LittleBigPlanet1)
{ {
User? user = await this.database.Users.Include(u => u.Location).FirstOrDefaultAsync(u => u.Username == username); User? user = await this.database.Users.Include(u => u.Location).FirstOrDefaultAsync(u => u.Username == username);
return user?.Serialize(gameVersion); return user?.Serialize(gameVersion);
@ -38,7 +38,7 @@ public class UserController : ControllerBase
GameToken? token = await this.database.GameTokenFromRequest(this.Request); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
string? user = await this.GetSerializedUser(username, token.GameVersion); string? user = await this.getSerializedUser(username, token.GameVersion);
if (user == null) return this.NotFound(); if (user == null) return this.NotFound();
return this.Ok(user); return this.Ok(user);
@ -51,7 +51,7 @@ public class UserController : ControllerBase
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
List<string?> serializedUsers = new(); List<string?> serializedUsers = new();
foreach (string userId in u) serializedUsers.Add(await this.GetSerializedUser(userId, token.GameVersion)); foreach (string userId in u) serializedUsers.Add(await this.getSerializedUser(userId, token.GameVersion));
string serialized = serializedUsers.Aggregate(string.Empty, (current, user) => user == null ? current : current + user); string serialized = serializedUsers.Aggregate(string.Empty, (current, user) => user == null ? current : current + user);

View file

@ -1,36 +0,0 @@
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.News;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/news")]
[Produces("text/xml")]
public class NewsController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
string newsEntry = LbpSerializer.StringElement
(
"item",
new NewsEntry
{
Category = "no_category",
Summary = "test summary",
Image = new NewsImage
{
Hash = "4947269c5f7061b27225611ee58a9a91a8031bbe",
Alignment = "right",
},
Id = 1,
Title = "Test Title",
Text = "Test Text",
Date = 1348755214000,
}.Serialize()
);
return this.Ok(LbpSerializer.StringElement("news", newsEntry));
}
}

View file

@ -1,7 +1,4 @@
#nullable enable #nullable enable
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin; namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin;
@ -16,14 +13,4 @@ public class AdminPanelController : ControllerBase
{ {
this.database = database; this.database = database;
} }
[HttpGet("testWebhook")]
public async Task<IActionResult> TestWebhook()
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
await WebhookHelper.SendWebhook("Testing 123", "Someone is testing the Discord webhook from the admin panel.");
return this.Redirect("/admin");
}
} }

View file

@ -1,15 +1,14 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Categories; using LBPUnion.ProjectLighthouse.Types.Categories;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reviews; using LBPUnion.ProjectLighthouse.Types.Reviews;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
using LBPUnion.ProjectLighthouse.Types.Tickets;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -67,9 +66,9 @@ public class Database : DbContext
} }
#nullable enable #nullable enable
public async Task<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "") public async Task<GameToken?> AuthenticateUser(NPTicket npTicket, string userLocation)
{ {
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username); User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
if (user == null) return null; if (user == null) return null;
GameToken gameToken = new() GameToken gameToken = new()
@ -78,15 +77,9 @@ public class Database : DbContext
User = user, User = user,
UserId = user.UserId, UserId = user.UserId,
UserLocation = userLocation, UserLocation = userLocation,
GameVersion = GameVersionHelper.FromTitleId(titleId), GameVersion = npTicket.GameVersion,
}; };
if (gameToken.GameVersion == GameVersion.Unknown)
{
Logger.Log($"Unknown GameVersion for TitleId {titleId}", LoggerLevelLogin.Instance);
return null;
}
this.GameTokens.Add(gameToken); this.GameTokens.Add(gameToken);
await this.SaveChangesAsync(); await this.SaveChangesAsync();

View file

@ -23,6 +23,10 @@ public static class BinaryReaderExtensions
public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0); public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0);
public static ulong ReadUInt64BE(this BinaryReader binRdr) => BitConverter.ToUInt32(binRdr.ReadBytesRequired(sizeof(ulong)).Reverse(), 0);
public static long ReadInt64BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(long)).Reverse(), 0);
public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount) public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount)
{ {
byte[] result = binRdr.ReadBytes(byteCount); byte[] result = binRdr.ReadBytes(byteCount);

View file

@ -99,6 +99,6 @@ public class GameVersionHelper
if (LittleBigPlanetVitaTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetVita; if (LittleBigPlanetVitaTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetVita;
if (LittleBigPlanetPSPTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetPSP; if (LittleBigPlanetPSPTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetPSP;
return GameVersion.LittleBigPlanet1; return GameVersion.Unknown;
} }
} }

View file

@ -14,7 +14,7 @@ public static class StatisticsHelper
public static async Task<int> UserCount() => await database.Users.CountAsync(u => !u.Banned); public static async Task<int> UserCount() => await database.Users.CountAsync(u => !u.Banned);
public static async Task<int> MMPicksCount() => await database.Slots.CountAsync(s => s.TeamPick); public static async Task<int> TeamPickCount() => await database.Slots.CountAsync(s => s.TeamPick);
public static async Task<int> PhotoCount() => await database.Photos.CountAsync(); public static async Task<int> PhotoCount() => await database.Photos.CountAsync();
} }

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace LBPUnion.ProjectLighthouse.Helpers;
public class SwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
List<KeyValuePair<string, OpenApiPathItem>> nonApiRoutes = swaggerDoc.Paths.Where(x => !x.Key.ToLower().StartsWith("/api/v1")).ToList();
nonApiRoutes.ForEach(x => swaggerDoc.Paths.Remove(x.Key));
}
}

View file

@ -7,7 +7,7 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static class WebhookHelper public static class WebhookHelper
{ {
private static readonly DiscordWebhookClient client = new(ServerSettings.Instance.DiscordWebhookUrl); private static readonly DiscordWebhookClient client = (ServerSettings.Instance.DiscordWebhookEnabled ? new DiscordWebhookClient(ServerSettings.Instance.DiscordWebhookUrl) : null);
public static readonly Color UnionColor = new(0, 140, 255); public static readonly Color UnionColor = new(0, 140, 255);
public static Task SendWebhook(EmbedBuilder builder) => SendWebhook(builder.Build()); public static Task SendWebhook(EmbedBuilder builder) => SendWebhook(builder.Build());

View file

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Maintenance.Commands;
[UsedImplicitly]
public class TestWebhookCommand : ICommand
{
public async Task Run(string[] args)
{
await WebhookHelper.SendWebhook("Testing 123", "Someone is testing the Discord webhook from the admin panel.");
}
public string Name() => "Test Discord Webhook";
public string[] Aliases()
=> new[]
{
"testWebhook", "testDiscordWebhook",
};
public string Arguments() => "";
public int RequiredArgs() => 0;
}

View file

@ -1,7 +1,8 @@
@page "/admin" @page "/admin"
@using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Maintenance @using LBPUnion.ProjectLighthouse.Maintenance
@using LBPUnion.ProjectLighthouse.Types.Settings @using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Pages.Admin.AdminPanelPage @model LBPUnion.ProjectLighthouse.Pages.Admin.AdminPanelPage
@{ @{
@ -9,61 +10,63 @@
Model.Title = "Admin Panel"; Model.Title = "Admin Panel";
} }
<a href="/admin/users"> @if (!this.Request.IsMobile())
<div class="ui blue button">
View Users
</div>
</a>
@if (ServerSettings.Instance.DiscordWebhookEnabled)
{ {
<a href="/admin/testWebhook"> <div class="ui center aligned grid">
<div class="ui blue button"> @foreach (AdminPanelStatistic statistic in Model.Statistics)
Test Discord Webhook {
@await Html.PartialAsync("Partials/AdminPanelStatisticPartial", statistic)
}
</div> </div>
</a> <br>
}
else
{
@foreach (AdminPanelStatistic statistic in Model.Statistics)
{
@await Html.PartialAsync("Partials/AdminPanelStatisticPartial", statistic)
<br>
}
} }
<h2>Commands</h2> <h2>Commands</h2>
<div class="ui grid"> @foreach (ICommand command in MaintenanceHelper.Commands)
@foreach (ICommand command in MaintenanceHelper.Commands) {
{
<div class="four wide column">
<div class="ui blue segment"> <div class="ui blue segment">
<h3>@command.Name()</h3> <h3>@command.Name()</h3>
<form> <form>
<div class="ui input" style="width: 100%;"> @if (command.RequiredArgs() > 0)
{
<div class="ui input" style="width: @(Model.Request.IsMobile() ? 100 : 30)%;">
<input type="text" name="args" placeholder="@command.Arguments()"> <input type="text" name="args" placeholder="@command.Arguments()">
</div><br><br> </div>
<br>
<br>
}
<input type="text" name="command" style="display: none;" value="@command.FirstAlias"> <input type="text" name="command" style="display: none;" value="@command.FirstAlias">
<button type="submit" class="ui green button" style="width: 100%;"> <button type="submit" class="ui green button">
<i class="play icon"></i> <i class="play icon"></i>
Execute Execute
</button> </button>
</form> </form>
</div> </div>
</div> }
}
</div>
<h2>Maintenance Jobs</h2> <h2>Maintenance Jobs</h2>
<p> <p>
<b>Warning: Interrupting Lighthouse during maintenance may leave the database in an unclean state.</b> <b>Warning: Interrupting Lighthouse during maintenance may leave the database in an unclean state.</b>
</p> </p>
<div class="ui grid"> @foreach (IMaintenanceJob job in MaintenanceHelper.MaintenanceJobs)
@foreach (IMaintenanceJob job in MaintenanceHelper.MaintenanceJobs) {
{
<div class="four wide column">
<div class="ui red segment"> <div class="ui red segment">
<h3>@job.Name()</h3> <h3>@job.Name()</h3>
<p>@job.Description()</p> <p>@job.Description()</p>
<form> <form>
<input type="text" name="maintenanceJob" style="display: none;" value="@job.GetType().Name"> <input type="text" name="maintenanceJob" style="display: none;" value="@job.GetType().Name">
<button type="submit" class="ui green button" style="width: 100%;"> <button type="submit" class="ui green button">
<i class="play icon"></i> <i class="play icon"></i>
Execute Run Job
</button> </button>
</form> </form>
</div> </div>
</div> }
}
</div>

View file

@ -15,12 +15,18 @@ public class AdminPanelPage : BaseLayout
public AdminPanelPage(Database database) : base(database) public AdminPanelPage(Database database) : base(database)
{} {}
public List<AdminPanelStatistic> Statistics = new();
public async Task<IActionResult> OnGet([FromQuery] string? args, [FromQuery] string? command, [FromQuery] string? maintenanceJob) public async Task<IActionResult> OnGet([FromQuery] string? args, [FromQuery] string? command, [FromQuery] string? maintenanceJob)
{ {
User? user = this.Database.UserFromWebRequest(this.Request); User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login"); if (user == null) return this.Redirect("~/login");
if (!user.IsAdmin) return this.NotFound(); if (!user.IsAdmin) return this.NotFound();
this.Statistics.Add(new AdminPanelStatistic("Users", await StatisticsHelper.UserCount(), "users"));
this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount()));
this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount()));
if (!string.IsNullOrEmpty(command)) if (!string.IsNullOrEmpty(command))
{ {
args ??= ""; args ??= "";

View file

@ -11,9 +11,10 @@
<script> <script>
function onSubmit(form) { function onSubmit(form) {
const password = form['password']; const passwordInput = document.getElementById("password");
const passwordSubmit = document.getElementById("password-submit");
password.value = sha256(password.value); passwordSubmit.value = sha256(passwordInput.value);
return true; return true;
} }
@ -43,7 +44,8 @@
<div class="field"> <div class="field">
<label>Password</label> <label>Password</label>
<div class="ui left icon input"> <div class="ui left icon input">
<input type="password" name="password" id="password" placeholder="Password"> <input type="password" id="password" placeholder="Password">
<input type="hidden" id="password-submit" name="password">
<i class="lock icon"></i> <i class="lock icon"></i>
</div> </div>
</div> </div>

View file

@ -0,0 +1,17 @@
@model LBPUnion.ProjectLighthouse.Types.AdminPanelStatistic
<div class="three wide column">
<div class="ui center aligned blue segment">
@if (Model.ViewAllEndpoint != null)
{
<h2>
<a href="/admin/@Model.ViewAllEndpoint">@Model.StatisticNamePlural</a>
</h2>
}
else
{
<h2>@Model.StatisticNamePlural</h2>
}
<h3>@Model.Count</h3>
</div>
</div>

View file

@ -10,6 +10,7 @@
await using Database database = new(); await using Database database = new();
string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name; string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool isQueued = false; bool isQueued = false;
bool isHearted = false; bool isHearted = false;
@ -27,23 +28,26 @@
string iconHash = Model.IconHash; string iconHash = Model.IconHash;
if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerSettings.Instance.MissingIconHash; if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerSettings.Instance.MissingIconHash;
} }
<div style="display: flex; width: 100%;"> <div class="card">
<div style="margin-right: 10px; background-image: url('/gameAssets/@iconHash'); height: 100px; width: 100px; background-size: cover; background-position: center; border-radius: 100%;"> @{
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> </div>
<div style="height: fit-content; vertical-align: center; align-self: center"> <div class="cardStats">
@if (showLink) @if (showLink)
{ {
<h2 style="margin-bottom: 2px;"> <h2>
<a href="~/slot/@Model.SlotId">@slotName</a> <a href="~/slot/@Model.SlotId">@slotName</a>
</h2> </h2>
} }
else else
{ {
<h1 style="margin-bottom: 2px;"> <h1>
@slotName @slotName
</h1> </h1>
} }
<div class="statsUnderTitle" style="margin-bottom: 10px;"> <div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span> <i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue play icon" title="Plays"></i> <span>@Model.Plays</span> <i class="blue play icon" title="Plays"></i> <span>@Model.Plays</span>
<i class="green thumbs up icon" title="Yays"></i> <span>@Model.Thumbsup</span> <i class="green thumbs up icon" title="Yays"></i> <span>@Model.Thumbsup</span>
@ -59,7 +63,7 @@
<i>Created by <a href="/user/@Model.Creator?.UserId">@Model.Creator?.Username</a> for @Model.GameVersion.ToPrettyString()</i> <i>Created by <a href="/user/@Model.Creator?.UserId">@Model.Creator?.Username</a> for @Model.GameVersion.ToPrettyString()</i>
</p> </p>
</div> </div>
<div style="height: 100px; margin-left: auto"> <div class="cardButtons">
<br> <br>
@if (user != null) @if (user != null)
{ {

View file

@ -3,12 +3,16 @@
@{ @{
bool showLink = (bool?)ViewData["ShowLink"] ?? false; bool showLink = (bool?)ViewData["ShowLink"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
} }
<div style="display: flex;"> <div class="card">
<div style="margin-right: 10px; background-image: url('/gameAssets/@Model.WebsiteAvatarHash'); height: 100px; width: 100px; background-size: cover; background-position: center; border-radius: .28571429rem;"> @{
int size = isMobile ? 50 : 100;
}
<div class="cardIcon userCardIcon" style="background-image: url('/gameAssets/@Model.WebsiteAvatarHash'); min-width: @(size)px; width: @(size)px; height: @(size)px">
</div> </div>
<div style="height: fit-content; vertical-align: center; align-self: center"> <div class="cardStats">
@if (showLink) @if (showLink)
{ {
<h2 style="margin-bottom: 2px;"> <h2 style="margin-bottom: 2px;">
@ -24,7 +28,7 @@
<p> <p>
<i>@Model.Status</i> <i>@Model.Status</i>
</p> </p>
<div class="statsUnderTitle"> <div class="cardStatsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span> <i class="pink heart icon" title="Hearts"></i> <span>@Model.Hearts</span>
<i class="blue comment icon" title="Comments"></i> <span>@Model.Comments</span> <i class="blue comment icon" title="Comments"></i> <span>@Model.Comments</span>
<i class="green upload icon" title="Uploaded Levels"></i><span>@Model.UsedSlots / @ServerSettings.Instance.EntitledSlots</span> <i class="green upload icon" title="Uploaded Levels"></i><span>@Model.UsedSlots / @ServerSettings.Instance.EntitledSlots</span>

View file

@ -9,6 +9,14 @@
<p>There are @Model.PhotoCount total photos!</p> <p>There are @Model.PhotoCount total photos!</p>
<form action="/photos/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search photos..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (Photo photo in Model.Photos) @foreach (Photo photo in Model.Photos)
{ {
<div class="ui segment"> <div class="ui segment">
@ -18,10 +26,10 @@
@if (Model.PageNumber != 0) @if (Model.PageNumber != 0)
{ {
<a href="/photos/@(Model.PageNumber - 1)">Previous Page</a> <a href="/photos/@(Model.PageNumber - 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
} }
@(Model.PageNumber + 1) / @(Model.PageAmount) @(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1) @if (Model.PageNumber < Model.PageAmount - 1)
{ {
<a href="/photos/@(Model.PageNumber + 1)">Next Page</a> <a href="/photos/@(Model.PageNumber + 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
} }

View file

@ -1,9 +1,9 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,25 @@ public class PhotosPage : BaseLayout
public int PhotoCount; public int PhotoCount;
public List<Photo> Photos; public List<Photo> Photos;
public string SearchValue;
public PhotosPage([NotNull] Database database) : base(database) public PhotosPage([NotNull] Database database) : base(database)
{} {}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber) public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{ {
this.PhotoCount = await StatisticsHelper.PhotoCount(); if (string.IsNullOrWhiteSpace(name)) name = "";
this.PhotoCount = await this.Database.Photos.CountAsync(p => p.Creator.Username.Contains(name) || p.PhotoSubjectCollection.Contains(name));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Photos = await this.Database.Photos.Include this.Photos = await this.Database.Photos.Include(p => p.Creator)
(p => p.Creator) .Where(p => p.Creator.Username.Contains(name) || p.PhotoSubjectCollection.Contains(name))
.OrderByDescending(p => p.Timestamp) .OrderByDescending(p => p.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -10,11 +10,12 @@
<script> <script>
function onSubmit(form) { function onSubmit(form) {
const password = form['password']; const passwordInput = document.getElementById("password");
const confirmPassword = form['confirmPassword']; const confirmPasswordInput = document.getElementById("confirmPassword");
const passwordSubmit = document.getElementById("password-submit");
password.value = sha256(password.value); const confirmPasswordSubmit = document.getElementById("confirm-submit");
confirmPassword.value = sha256(confirmPassword.value); passwordSubmit.value = sha256(passwordInput.value);
confirmPasswordSubmit.value = sha256(confirmPasswordInput.value);
return true; return true;
} }
@ -44,7 +45,8 @@
<div class="field"> <div class="field">
<label>Password</label> <label>Password</label>
<div class="ui left icon input"> <div class="ui left icon input">
<input type="password" name="password" id="password" placeholder="Password"> <input type="password" id="password" placeholder="Password">
<input type="hidden" name="password" id="password-submit">
<i class="lock icon"></i> <i class="lock icon"></i>
</div> </div>
</div> </div>
@ -52,7 +54,8 @@
<div class="field"> <div class="field">
<label>Confirm Password</label> <label>Confirm Password</label>
<div class="ui left icon input"> <div class="ui left icon input">
<input type="password" name="confirmPassword" id="confirmPassword" placeholder="Confirm Password"> <input type="password" id="confirmPassword" placeholder="Confirm Password">
<input type="hidden" name="confirmPassword" id="confirm-submit">
<i class="lock icon"></i> <i class="lock icon"></i>
</div> </div>
</div> </div>

View file

@ -1,4 +1,5 @@
@page "/slot/{id:int}" @page "/slot/{id:int}"
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@model LBPUnion.ProjectLighthouse.Pages.SlotPage @model LBPUnion.ProjectLighthouse.Pages.SlotPage
@{ @{
@ -20,6 +21,9 @@
{ {
"ShowLink", false "ShowLink", false
}, },
{
"IsMobile", Model.Request.IsMobile()
},
}) })
<br> <br>

View file

@ -1,4 +1,5 @@
@page "/slots/{pageNumber:int}" @page "/slots/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types.Levels @using LBPUnion.ProjectLighthouse.Types.Levels
@model LBPUnion.ProjectLighthouse.Pages.SlotsPage @model LBPUnion.ProjectLighthouse.Pages.SlotsPage
@ -9,8 +10,17 @@
<p>There are @Model.SlotCount total levels!</p> <p>There are @Model.SlotCount total levels!</p>
<form action="/slots/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search levels..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (Slot slot in Model.Slots) @foreach (Slot slot in Model.Slots)
{ {
bool isMobile = Model.Request.IsMobile();
<div class="ui segment"> <div class="ui segment">
@await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData) @await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData)
{ {
@ -23,16 +33,19 @@
{ {
"ShowLink", true "ShowLink", true
}, },
{
"IsMobile", isMobile
},
}) })
</div> </div>
} }
@if (Model.PageNumber != 0) @if (Model.PageNumber != 0)
{ {
<a href="/slots/@(Model.PageNumber - 1)">Previous Page</a> <a href="/slots/@(Model.PageNumber - 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
} }
@(Model.PageNumber + 1) / @(Model.PageAmount) @(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1) @if (Model.PageNumber < Model.PageAmount - 1)
{ {
<a href="/slots/@(Model.PageNumber + 1)">Next Page</a> <a href="/slots/@(Model.PageNumber + 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
} }

View file

@ -1,9 +1,9 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,26 @@ public class SlotsPage : BaseLayout
public int SlotCount; public int SlotCount;
public List<Slot> Slots; public List<Slot> Slots;
public string SearchValue;
public SlotsPage([NotNull] Database database) : base(database) public SlotsPage([NotNull] Database database) : base(database)
{} {}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber) public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{ {
this.SlotCount = await StatisticsHelper.SlotCount(); if (string.IsNullOrWhiteSpace(name)) name = "";
this.SlotCount = await this.Database.Slots.CountAsync(p => p.Name.Contains(name));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Slots = await this.Database.Slots.Include this.Slots = await this.Database.Slots.Include(p => p.Creator)
(p => p.Creator) .Where(p => p.Name.Contains(name))
.OrderByDescending(p => p.FirstUploaded) .OrderByDescending(p => p.FirstUploaded)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -1,6 +1,7 @@
@page "/user/{userId:int}" @page "/user/{userId:int}"
@using System.IO @using System.IO
@using System.Web @using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types
@using LBPUnion.ProjectLighthouse.Types.Profiles @using LBPUnion.ProjectLighthouse.Types.Profiles
@model LBPUnion.ProjectLighthouse.Pages.UserPage @model LBPUnion.ProjectLighthouse.Pages.UserPage
@ -43,6 +44,9 @@
{ {
"ShowLink", false "ShowLink", false
}, },
{
"IsMobile", Model.Request.IsMobile()
},
}) })
</div> </div>
<div class="eight wide right aligned column"> <div class="eight wide right aligned column">

View file

@ -27,7 +27,7 @@ public class UserPage : BaseLayout
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound(); if (this.ProfileUser == null) return this.NotFound();
this.Photos = await this.Database.Photos.OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(5).ToListAsync(); 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 this.Comments = await this.Database.Comments.Include
(p => p.Poster) (p => p.Poster)
.Include(p => p.Target) .Include(p => p.Target)

View file

@ -1,4 +1,5 @@
@page "/users/{pageNumber:int}" @page "/users/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Pages.UsersPage @model LBPUnion.ProjectLighthouse.Pages.UsersPage
@ -9,24 +10,36 @@
<p>There are @Model.UserCount total users.</p> <p>There are @Model.UserCount total users.</p>
<form action="/users/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search users..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
@foreach (User user in Model.Users) @foreach (User user in Model.Users)
{ {
bool isMobile = Model.Request.IsMobile();
<div class="ui segment"> <div class="ui segment">
@await Html.PartialAsync("Partials/UserCardPartial", user, new ViewDataDictionary(ViewData) @await Html.PartialAsync("Partials/UserCardPartial", user, new ViewDataDictionary(ViewData)
{ {
{ {
"ShowLink", true "ShowLink", true
}, },
{
"IsMobile", isMobile
},
}) })
</div> </div>
} }
@if (Model.PageNumber != 0) @if (Model.PageNumber != 0)
{ {
<a href="/users/@(Model.PageNumber - 1)">Previous Page</a> <a href="/users/@(Model.PageNumber - 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
} }
@(Model.PageNumber + 1) / @(Model.PageAmount) @(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1) @if (Model.PageNumber < Model.PageAmount - 1)
{ {
<a href="/users/@(Model.PageNumber + 1)">Next Page</a> <a href="/users/@(Model.PageNumber + 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
} }

View file

@ -1,9 +1,9 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,24 @@ public class UsersPage : BaseLayout
public List<User> Users; public List<User> Users;
public string SearchValue;
public UsersPage([NotNull] Database database) : base(database) public UsersPage([NotNull] Database database) : base(database)
{} {}
public async Task<IActionResult> OnGet([FromRoute] int pageNumber) public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{ {
this.UserCount = await StatisticsHelper.UserCount(); if (string.IsNullOrWhiteSpace(name)) name = "";
this.UserCount = await this.Database.Users.CountAsync(u => !u.Banned && u.Username.Contains(name));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Users = await this.Database.Users.Where this.Users = await this.Database.Users.Where(u => !u.Banned && u.Username.Contains(name))
(u => !u.Banned)
.OrderByDescending(b => b.UserId) .OrderByDescending(b => b.UserId)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -82,6 +82,7 @@ public static class Program
return; return;
} }
FileHelper.EnsureDirectoryCreated(Path.Combine(Environment.CurrentDirectory, "png"));
if (Directory.Exists("r")) if (Directory.Exists("r"))
{ {
Logger.Log Logger.Log

View file

@ -7,6 +7,11 @@
<RootNamespace>LBPUnion.ProjectLighthouse</RootNamespace> <RootNamespace>LBPUnion.ProjectLighthouse</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2"/> <PackageReference Include="BCrypt.Net-Next" Version="4.0.2"/>
<PackageReference Include="DDSReader" Version="1.0.8-pre"/> <PackageReference Include="DDSReader" Version="1.0.8-pre"/>
@ -23,6 +28,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0"/> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0"/>
<PackageReference Include="SharpZipLib" Version="1.3.3"/> <PackageReference Include="SharpZipLib" Version="1.3.3"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,5 +1,7 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection;
using Kettu; using Kettu;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
@ -13,8 +15,11 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.OpenApi.Models;
#if RELEASE
using Microsoft.Extensions.Hosting.Internal;
#endif
namespace LBPUnion.ProjectLighthouse.Startup; namespace LBPUnion.ProjectLighthouse.Startup;
@ -56,6 +61,30 @@ public class Startup
} }
); );
services.AddSwaggerGen
(
c =>
{
// Give swagger the name and version of our project
c.SwaggerDoc
(
"v1",
new OpenApiInfo
{
Title = "Project Lighthouse API",
Version = "v1",
}
);
// Filter out endpoints not in /api/v1
c.DocumentFilter<SwaggerFilter>();
// Add XMLDoc to swagger
string xmlDocs = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlDocs));
}
);
#if DEBUG #if DEBUG
services.AddSingleton<IHostLifetime, DebugWarmupLifetime>(); services.AddSingleton<IHostLifetime, DebugWarmupLifetime>();
#else #else
@ -85,6 +114,15 @@ public class Startup
app.UseForwardedHeaders(); app.UseForwardedHeaders();
app.UseSwagger();
app.UseSwaggerUI
(
c =>
{
c.SwaggerEndpoint("v1/swagger.json", "Project Lighthouse API");
}
);
// Logs every request and the response to it // Logs every request and the response to it
// Example: "200, 13ms: GET /LITTLEBIGPLANETPS3_XML/news" // Example: "200, 13ms: GET /LITTLEBIGPLANETPS3_XML/news"
// Example: "404, 127ms: GET /asdasd?query=osucookiezi727ppbluezenithtopplayhdhr" // Example: "404, 127ms: GET /asdasd?query=osucookiezi727ppbluezenithtopplayhdhr"

View file

@ -8,14 +8,75 @@ div.main {
flex: 1; flex: 1;
} }
div.statsUnderTitle > i {
margin-right: 2px;
}
div.statsUnderTitle > span {
margin-right: 5px;
}
#lighthouse-debug-info > p { #lighthouse-debug-info > p {
margin-bottom: 1px; margin-bottom: 1px;
} }
/*#region Cards*/
.card {
display: flex;
width: 100%;
}
.cardIcon {
margin-right: 10px;
background-size: cover;
background-position: center;
border-radius: 100%;
}
.cardStats {
height: fit-content;
vertical-align: center;
align-self: center
}
.cardStats > h1,
.cardStats > h2 {
margin-bottom: 2px;
}
.cardButtons {
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
margin-left: auto;
}
.cardButtons > a {
margin-bottom: 5px !important;
vertical-align: center;
}
.cardStatsUnderTitle {
margin-bottom: 10px;
}
div.cardStatsUnderTitle > i {
margin-right: 2px;
}
div.cardStatsUnderTitle > span {
margin-right: 5px;
}
/*#region Slot cards*/
.slotCardIcon {
border-radius: 100%;
}
/*#endregion Slot cards*/
/*#region User cards*/
.userCardIcon {
border-radius: .28571429rem;
}
/*#endregion User cards*/
/*#endregion Cards*/

View file

@ -0,0 +1,18 @@
#nullable enable
namespace LBPUnion.ProjectLighthouse.Types;
public struct AdminPanelStatistic
{
public AdminPanelStatistic(string statisticNamePlural, int count, string? viewAllEndpoint = null)
{
this.StatisticNamePlural = statisticNamePlural;
this.Count = count;
this.ViewAllEndpoint = viewAllEndpoint;
}
public readonly string StatisticNamePlural;
public readonly int Count;
public readonly string? ViewAllEndpoint;
}

View file

@ -0,0 +1,10 @@
namespace LBPUnion.ProjectLighthouse.Types.Api;
public class StatisticsResponse
{
public int RecentMatches { get; set; }
public int Slots { get; set; }
public int Users { get; set; }
public int TeamPicks { get; set; }
public int Photos { get; set; }
}

View file

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Types;
[ApiController]
[Route("/api/v1/")]
[Produces("application/json")]
public class ApiEndpointController : ControllerBase
{}

View file

@ -0,0 +1,26 @@
namespace LBPUnion.ProjectLighthouse.Types.Levels;
public struct MinimalSlot
{
public int SlotId { get; set; }
public string Name { get; set; }
public string IconHash { get; set; }
public bool TeamPick { get; set; }
public GameVersion GameVersion { get; set; }
#if DEBUG
public long FirstUploaded { get; set; }
#endif
public static MinimalSlot FromSlot(Slot slot)
=> new()
{
SlotId = slot.SlotId,
Name = slot.Name,
IconHash = slot.IconHash,
TeamPick = slot.TeamPick,
GameVersion = slot.GameVersion,
#if DEBUG
FirstUploaded = slot.FirstUploaded,
#endif
};
}

View file

@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
@ -19,6 +20,7 @@ public class Slot
{ {
[XmlAttribute("type")] [XmlAttribute("type")]
[NotMapped] [NotMapped]
[JsonIgnore]
public string Type { get; set; } = "user"; public string Type { get; set; } = "user";
[Key] [Key]
@ -35,24 +37,29 @@ public class Slot
public string IconHash { get; set; } = ""; public string IconHash { get; set; } = "";
[XmlElement("rootLevel")] [XmlElement("rootLevel")]
[JsonIgnore]
public string RootLevel { get; set; } = ""; public string RootLevel { get; set; } = "";
[JsonIgnore]
public string ResourceCollection { get; set; } = ""; public string ResourceCollection { get; set; } = "";
[NotMapped] [NotMapped]
[XmlElement("resource")] [XmlElement("resource")]
[JsonIgnore]
public string[] Resources { public string[] Resources {
get => this.ResourceCollection.Split(","); get => this.ResourceCollection.Split(",");
set => this.ResourceCollection = string.Join(',', value); set => this.ResourceCollection = string.Join(',', value);
} }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int LocationId { get; set; } public int LocationId { get; set; }
[XmlIgnore] [XmlIgnore]
public int CreatorId { get; set; } public int CreatorId { get; set; }
[ForeignKey(nameof(CreatorId))] [ForeignKey(nameof(CreatorId))]
[JsonIgnore]
public User? Creator { get; set; } public User? Creator { get; set; }
/// <summary> /// <summary>
@ -60,6 +67,7 @@ public class Slot
/// </summary> /// </summary>
[XmlElement("location")] [XmlElement("location")]
[ForeignKey(nameof(LocationId))] [ForeignKey(nameof(LocationId))]
[JsonIgnore]
public Location? Location { get; set; } public Location? Location { get; set; }
[XmlElement("initiallyLocked")] [XmlElement("initiallyLocked")]
@ -78,6 +86,7 @@ public class Slot
public string AuthorLabels { get; set; } = ""; public string AuthorLabels { get; set; } = "";
[XmlElement("background")] [XmlElement("background")]
[JsonIgnore]
public string BackgroundHash { get; set; } = ""; public string BackgroundHash { get; set; } = "";
[XmlElement("minPlayers")] [XmlElement("minPlayers")]
@ -103,6 +112,7 @@ public class Slot
[XmlIgnore] [XmlIgnore]
[NotMapped] [NotMapped]
[JsonIgnore]
public int Hearts { public int Hearts {
get { get {
using Database database = new(); using Database database = new();
@ -124,42 +134,55 @@ public class Slot
public int PlaysComplete => this.PlaysLBP1Complete + this.PlaysLBP2Complete + this.PlaysLBP3Complete + this.PlaysLBPVitaComplete; public int PlaysComplete => this.PlaysLBP1Complete + this.PlaysLBP2Complete + this.PlaysLBP3Complete + this.PlaysLBPVitaComplete;
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP1 { get; set; } public int PlaysLBP1 { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP1Complete { get; set; } public int PlaysLBP1Complete { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP1Unique { get; set; } public int PlaysLBP1Unique { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP2 { get; set; } public int PlaysLBP2 { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP2Complete { get; set; } public int PlaysLBP2Complete { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP2Unique { get; set; } public int PlaysLBP2Unique { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP3 { get; set; } public int PlaysLBP3 { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP3Complete { get; set; } public int PlaysLBP3Complete { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBP3Unique { get; set; } public int PlaysLBP3Unique { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBPVita { get; set; } public int PlaysLBPVita { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBPVitaComplete { get; set; } public int PlaysLBPVitaComplete { get; set; }
[XmlIgnore] [XmlIgnore]
[JsonIgnore]
public int PlaysLBPVitaUnique { get; set; } public int PlaysLBPVitaUnique { get; set; }
[NotMapped] [NotMapped]
[JsonIgnore]
[XmlElement("thumbsup")] [XmlElement("thumbsup")]
public int Thumbsup { public int Thumbsup {
get { get {
@ -170,6 +193,7 @@ public class Slot
} }
[NotMapped] [NotMapped]
[JsonIgnore]
[XmlElement("thumbsdown")] [XmlElement("thumbsdown")]
public int Thumbsdown { public int Thumbsdown {
get { get {
@ -180,6 +204,7 @@ public class Slot
} }
[NotMapped] [NotMapped]
[JsonPropertyName("averageRating")]
[XmlElement("averageRating")] [XmlElement("averageRating")]
public double RatingLBP1 { public double RatingLBP1 {
get { get {
@ -193,6 +218,7 @@ public class Slot
} }
[NotMapped] [NotMapped]
[JsonIgnore]
[XmlElement("reviewCount")] [XmlElement("reviewCount")]
public int ReviewCount { public int ReviewCount {
get { get {

View file

@ -1,45 +0,0 @@
#nullable enable
using System;
using System.IO;
using System.Text;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Types;
/// <summary>
/// The data sent from POST /LOGIN.
/// </summary>
public class LoginData
{
public static readonly string UsernamePrefix = Encoding.ASCII.GetString
(
new byte[]
{
0x04, 0x00, 0x20,
}
);
public string Username { get; set; } = null!;
/// <summary>
/// Converts a X-I-5 Ticket into `LoginData`.
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
/// </summary>
public static LoginData? CreateFromString(string str)
{
str = str.Replace("\b", ""); // Remove backspace characters
using MemoryStream ms = new(Encoding.ASCII.GetBytes(str));
using BinaryReader reader = new(ms);
if (!str.Contains(UsernamePrefix)) return null;
LoginData loginData = new();
reader.BaseStream.Position = str.IndexOf(UsernamePrefix, StringComparison.Ordinal) + UsernamePrefix.Length;
loginData.Username = BinaryHelper.ReadString(reader).Replace("\0", string.Empty);
return loginData;
}
}

View file

@ -1,26 +0,0 @@
using LBPUnion.ProjectLighthouse.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.News;
/// <summary>
/// Used on the info moon on LBP1. Broken for unknown reasons
/// </summary>
public class NewsEntry
{
public int Id { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public string Text { get; set; }
public NewsImage Image { get; set; }
public string Category { get; set; }
public long Date { get; set; }
public string Serialize()
=> LbpSerializer.StringElement("id", this.Id) +
LbpSerializer.StringElement("title", this.Title) +
LbpSerializer.StringElement("summary", this.Summary) +
LbpSerializer.StringElement("text", this.Text) +
LbpSerializer.StringElement("date", this.Date) +
this.Image.Serialize() +
LbpSerializer.StringElement("category", this.Category);
}

View file

@ -1,12 +0,0 @@
using LBPUnion.ProjectLighthouse.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.News;
public class NewsImage
{
public string Hash { get; set; }
public string Alignment { get; set; }
public string Serialize()
=> LbpSerializer.StringElement("image", LbpSerializer.StringElement("hash", this.Hash) + LbpSerializer.StringElement("alignment", this.Alignment));
}

View file

@ -5,4 +5,7 @@ public enum Platform
PS3 = 0, PS3 = 0,
RPCS3 = 1, RPCS3 = 1,
Vita = 2, Vita = 2,
PSP = 3,
UnitTest = 4,
Unknown = -1,
} }

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public struct DataHeader
{
public DataType Type;
public ushort Length;
}

View file

@ -0,0 +1,11 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public enum DataType : byte
{
Empty = 0x00,
UInt32 = 0x01,
UInt64 = 0x02,
String = 0x04,
Timestamp = 0x07,
Binary = 0x08,
}

View file

@ -0,0 +1,188 @@
#nullable enable
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Settings;
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
/// <summary>
/// A PSN ticket, typically sent from PS3/RPCN
/// </summary>
public class NPTicket
{
public string Username { get; set; }
private Version ticketVersion { get; set; }
public Platform Platform { get; set; }
public uint IssuerId { get; set; }
public ulong IssuedDate { get; set; }
public ulong ExpireDate { get; set; }
private string titleId { get; set; }
public GameVersion GameVersion { get; set; }
private static void Read21Ticket(NPTicket npTicket, TicketReader reader)
{
reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for
npTicket.IssuerId = reader.ReadTicketUInt32();
npTicket.IssuedDate = reader.ReadTicketUInt64();
npTicket.ExpireDate = reader.ReadTicketUInt64();
reader.ReadTicketUInt64(); // PSN User id, we don't care about this
npTicket.Username = reader.ReadTicketString();
reader.ReadTicketString(); // Country
reader.ReadTicketString(); // Domain
npTicket.titleId = reader.ReadTicketString();
}
// Function is here for future use incase we ever need to read more from the ticket
private static void Read30Ticket(NPTicket npTicket, TicketReader reader)
{
Read21Ticket(npTicket, reader);
}
/// <summary>
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
/// </summary>
public static NPTicket? CreateFromBytes(byte[] data)
{
NPTicket npTicket = new();
#if DEBUG
if (data[0] == 'u' && ServerStatics.IsUnitTesting)
{
string dataStr = Encoding.UTF8.GetString(data);
if (dataStr.StartsWith("unitTestTicket"))
{
npTicket = new NPTicket
{
IssuerId = 0,
ticketVersion = new Version(0, 0),
Platform = Platform.UnitTest,
GameVersion = GameVersion.LittleBigPlanet2,
ExpireDate = 0,
IssuedDate = 0,
};
npTicket.Username = dataStr.Substring(14);
return npTicket;
}
}
#endif
try
{
using MemoryStream ms = new(data);
using TicketReader reader = new(ms);
npTicket.ticketVersion = reader.ReadTicketVersion();
reader.ReadBytes(4); // Skip header
reader.ReadUInt16BE(); // Ticket length, we don't care about this
#if DEBUG
SectionHeader bodyHeader = reader.ReadSectionHeader();
Logger.Log($"bodyHeader.Type is {bodyHeader.Type}", LoggerLevelLogin.Instance);
#else
reader.ReadSectionHeader();
#endif
switch (npTicket.ticketVersion)
{
case "2.1":
Read21Ticket(npTicket, reader);
break;
case "3.0":
Read30Ticket(npTicket, reader);
break;
default: throw new NotImplementedException();
}
// We already read the title id, however we need to do some post-processing to get what we want.
// Current data: UP9000-BCUS98245_00
// We need to chop this to get the titleId we're looking for
npTicket.titleId = npTicket.titleId.Substring(7); // Trim UP9000-
npTicket.titleId = npTicket.titleId.Substring(0, npTicket.titleId.Length - 3); // Trim _00 at the end
// Data now (hopefully): BCUS98245
#if DEBUG
Logger.Log($"titleId is {npTicket.titleId}", LoggerLevelLogin.Instance);
#endif
npTicket.GameVersion = GameVersionHelper.FromTitleId(npTicket.titleId); // Finally, convert it to GameVersion
if (npTicket.GameVersion == GameVersion.Unknown)
{
Logger.Log($"Could not determine game version from title id {npTicket.titleId}", LoggerLevelLogin.Instance);
return null;
}
// Production PSN Issuer ID: 0x100
// RPCN Issuer ID: 0x33333333
npTicket.Platform = npTicket.IssuerId switch
{
0x100 => Platform.PS3,
0x33333333 => Platform.RPCS3,
_ => Platform.Unknown,
};
if (npTicket.Platform == Platform.PS3 && npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita;
if (npTicket.Platform == Platform.Unknown)
{
Logger.Log($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LoggerLevelLogin.Instance);
return null;
}
#if DEBUG
Logger.Log("npTicket data:", LoggerLevelLogin.Instance);
foreach (string line in JsonSerializer.Serialize(npTicket).Split('\n'))
{
Logger.Log(line, LoggerLevelLogin.Instance);
}
#endif
return npTicket;
}
catch(NotImplementedException)
{
Logger.Log($"The ticket version {npTicket.ticketVersion} is not implemented yet.", LoggerLevelLogin.Instance);
Logger.Log
(
"Please let us know that this is a ticket version that is actually used on our issue tracker at https://github.com/LBPUnion/project-lighthouse/issues !",
LoggerLevelLogin.Instance
);
return null;
}
catch(Exception e)
{
Logger.Log("Failed to read npTicket!", LoggerLevelLogin.Instance);
Logger.Log("Either this is spam data, or the more likely that this is a bug.", LoggerLevelLogin.Instance);
Logger.Log
(
"Please report the following exception to our issue tracker at https://github.com/LBPUnion/project-lighthouse/issues!",
LoggerLevelLogin.Instance
);
foreach (string line in e.ToDetailedException().Split('\n'))
{
Logger.Log(line, LoggerLevelLogin.Instance);
}
return null;
}
}
}

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public struct SectionHeader
{
public SectionType Type;
public ushort Length;
}

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public enum SectionType : byte
{
Body = 0x00,
Footer = 0x02,
}

View file

@ -0,0 +1,61 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public class TicketReader : BinaryReader
{
public TicketReader([NotNull] Stream input) : base(input)
{}
public Version ReadTicketVersion() => new(this.ReadByte() >> 4, this.ReadByte());
public SectionHeader ReadSectionHeader()
{
this.ReadByte();
SectionHeader sectionHeader = new();
sectionHeader.Type = (SectionType)this.ReadByte();
sectionHeader.Length = this.ReadUInt16BE();
return sectionHeader;
}
public DataHeader ReadDataHeader()
{
DataHeader dataHeader = new();
dataHeader.Type = (DataType)this.ReadUInt16BE();
dataHeader.Length = this.ReadUInt16BE();
return dataHeader;
}
public byte[] ReadTicketBinary()
{
DataHeader dataHeader = this.ReadDataHeader();
Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String);
return this.ReadBytes(dataHeader.Length);
}
public string ReadTicketString() => Encoding.UTF8.GetString(this.ReadTicketBinary()).TrimEnd('\0');
public uint ReadTicketUInt32()
{
DataHeader dataHeader = this.ReadDataHeader();
Debug.Assert(dataHeader.Type == DataType.UInt32);
return this.ReadUInt32BE();
}
public ulong ReadTicketUInt64()
{
DataHeader dataHeader = this.ReadDataHeader();
Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp);
return this.ReadUInt64BE();
}
}

View file

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
@ -13,11 +14,17 @@ public class User
public readonly ClientsConnected ClientsConnected = new(); public readonly ClientsConnected ClientsConnected = new();
public int UserId { get; set; } public int UserId { get; set; }
public string Username { get; set; } public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; } public string Password { get; set; }
public string IconHash { get; set; } public string IconHash { get; set; }
[JsonIgnore]
public int Game { get; set; } public int Game { get; set; }
[NotMapped] [NotMapped]
[JsonIgnore]
public int Lists => 0; public int Lists => 0;
/// <summary> /// <summary>
@ -26,6 +33,7 @@ public class User
public string Biography { get; set; } public string Biography { get; set; }
[NotMapped] [NotMapped]
[JsonIgnore]
public string WebsiteAvatarHash { public string WebsiteAvatarHash {
get { get {
string avatarHash = this.IconHash; string avatarHash = this.IconHash;
@ -40,6 +48,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int Reviews { public int Reviews {
get { get {
using Database database = new(); using Database database = new();
@ -48,6 +57,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int Comments { public int Comments {
get { get {
using Database database = new(); using Database database = new();
@ -56,6 +66,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int PhotosByMe { public int PhotosByMe {
get { get {
using Database database = new(); using Database database = new();
@ -64,6 +75,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int PhotosWithMe { public int PhotosWithMe {
get { get {
using Database database = new(); using Database database = new();
@ -71,15 +83,18 @@ public class User
} }
} }
[JsonIgnore]
public int LocationId { get; set; } public int LocationId { get; set; }
/// <summary> /// <summary>
/// The location of the profile card on the user's earth /// The location of the profile card on the user's earth
/// </summary> /// </summary>
[ForeignKey("LocationId")] [ForeignKey("LocationId")]
[JsonIgnore]
public Location Location { get; set; } public Location Location { get; set; }
[NotMapped] [NotMapped]
[JsonIgnore]
public int HeartedLevels { public int HeartedLevels {
get { get {
using Database database = new(); using Database database = new();
@ -88,6 +103,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int HeartedUsers { public int HeartedUsers {
get { get {
using Database database = new(); using Database database = new();
@ -96,6 +112,7 @@ public class User
} }
[NotMapped] [NotMapped]
[JsonIgnore]
public int QueuedLevels { public int QueuedLevels {
get { get {
using Database database = new(); using Database database = new();
@ -103,10 +120,13 @@ public class User
} }
} }
[JsonIgnore]
public string Pins { get; set; } = ""; public string Pins { get; set; } = "";
[JsonIgnore]
public string PlanetHash { get; set; } = ""; public string PlanetHash { get; set; } = "";
[JsonIgnore]
public int Hearts { public int Hearts {
get { get {
using Database database = new(); using Database database = new();
@ -115,8 +135,10 @@ public class User
} }
} }
[JsonIgnore]
public bool IsAdmin { get; set; } = false; public bool IsAdmin { get; set; } = false;
[JsonIgnore]
public bool PasswordResetRequired { get; set; } public bool PasswordResetRequired { get; set; }
public string YayHash { get; set; } = ""; public string YayHash { get; set; } = "";
@ -125,6 +147,7 @@ public class User
#nullable enable #nullable enable
[NotMapped] [NotMapped]
[JsonIgnore]
public string Status { public string Status {
get { get {
using Database database = new(); using Database database = new();
@ -139,8 +162,10 @@ public class User
} }
#nullable disable #nullable disable
[JsonIgnore]
public bool Banned { get; set; } public bool Banned { get; set; }
[JsonIgnore]
public string BannedReason { get; set; } public string BannedReason { get; set; }
public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1) public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1)
@ -178,6 +203,7 @@ public class User
/// The number of used slots on the earth /// The number of used slots on the earth
/// </summary> /// </summary>
[NotMapped] [NotMapped]
[JsonIgnore]
public int UsedSlots { public int UsedSlots {
get { get {
using Database database = new(); using Database database = new();
@ -194,6 +220,7 @@ public class User
/// <summary> /// <summary>
/// The number of slots remaining on the earth /// The number of slots remaining on the earth
/// </summary> /// </summary>
[JsonIgnore]
public int FreeSlots => ServerSettings.Instance.EntitledSlots - this.UsedSlots; public int FreeSlots => ServerSettings.Instance.EntitledSlots - this.UsedSlots;
private static readonly string[] slotTypes = private static readonly string[] slotTypes =

View file

@ -0,0 +1,17 @@
namespace LBPUnion.ProjectLighthouse.Types;
public class Version
{
public int Major { get; set; }
public int Minor { get; set; }
public Version(int major, int minor)
{
this.Major = major;
this.Minor = minor;
}
public override string ToString() => $"{this.Major}.{this.Minor}";
public static implicit operator string(Version v) => v.ToString();
}

121
README.md
View file

@ -1,121 +1,38 @@
# Project Lighthouse # Project Lighthouse
Project Lighthouse is a clean-room, open-source custom server for LittleBigPlanet. This is a project conducted by Project Lighthouse is a clean-room, open-source custom server for LittleBigPlanet. This is a project conducted by
the [LBP Union Ministry of Technology Research and Development team.](https://www.lbpunion.com/technology) For concerns the [LBP Union Ministry of Technology Research and Development team](https://www.lbpunion.com/technology).
and inquiries about the project, please [contact us here.](https://www.lbpunion.com/contact) For general questions and
discussion about Project Lighthouse, please see For concerns and inquiries about the project, please contact us [here](https://www.lbpunion.com/contact).
For general questions and discussion about Project Lighthouse, please see
the [megathread](https://www.lbpunion.com/forum/union-hall/project-lighthouse-littlebigplanet-private-servers-megathread) the [megathread](https://www.lbpunion.com/forum/union-hall/project-lighthouse-littlebigplanet-private-servers-megathread)
on our forum. on our forum.
## WARNING! ## DISCLAIMER
This is **beta software**, and thus is **not stable**. This is **beta software**, and thus is **not stable nor is it secure**.
We're not responsible if someone hacks your machine and wipes your database. While Project Lighthouse is in a mostly working state, **we ask that our software not be used in a production
environment until release**.
Make frequent backups, and be sure to report any vulnerabilities. This is because we have not entirely nailed security down yet, and **your instance WILL get attacked** as a result. It's
happened before, and it'll happen again.
Simply put, **Project Lighthouse is not ready for the general public yet**.
In addition, we're not responsible if someone hacks your machine and wipes your database, so make frequent backups, and
be sure to report any vulnerabilities. Thank you in advance.
## Building ## Building
This will be written when we're out of beta. Consider this your barrier to entry ;). This will be written when we're out of beta. Consider this your barrier to entry ;).
It is recommended to build with Release if you plan to use Lighthouse in a production environment. It is recommended to build with `Release` if you plan to use Lighthouse in a production environment.
## Running ## Contributing
Lighthouse requires a MySQL database at this time. For Linux users running docker, one can be set up using Please see `CONTRIBUTING.md` for more information.
the `docker-compose.yml` file in the root of the project folder.
Next, make sure the `LIGHTHOUSE_DB_CONNECTION_STRING` environment variable is set correctly. By default, it
is `server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse`. If you are running the database via the
above `docker-compose.yml` you shouldn't need to change this. For other development/especially production environments
you will need to change this.
Once you've gotten MySQL running you can run Lighthouse. It will take care of the rest.
## Connecting
PS3 is difficult to set up, so I will be going over how to set up RPCS3 instead. A guide will be coming for PS3 closer
to release. You can also follow this guide if you want to learn how to modify your EBOOT.
There are also community-provided guides in [the official LBP Union Discord](https://www.lbpunion.com/discord), which
you can follow at your own discretion.
*Note: This requires a modified copy of RPCS3. You can find a working
version [on our GitHub](https://github.com/LBPUnion/rpcs3).*
Start by getting a copy of LittleBigPlanet 1/2 installed. (Check the LittleBigPlanet 1 section, since you'll need to do
extra steps for your game to not crash upon entering pod computer). It can be digital (NPUA80472/NPUA80662) or disc (
BCUS98148/BCUS98245). For those that don't, the [RPCS3 Quickstart Guide](https://rpcs3.net/quickstart) should cover it.
Next, download [UnionPatcher](https://github.com/LBPUnion/UnionPatcher/). Binaries can be found by reading the README.md
file.
You should have everything you need now, so open up RPCS3 and go to Utilities -> Decrypt PS3 Binaries. Point this
to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`. You can grab your title id by right clicking the game in RPCS3 and
clicking Copy Info -> Copy Serial.
This should give you a file named `EBOOT.elf` in the same folder. Next, fire up UnionPatcher (making sure to select the
correct project to start, e.g. on Mac launch `UnionPatcher.Gui.MacOS`.)
Now that you have your decrypted eboot, open UnionPatcher and select the `EBOOT.elf` you got earlier in the top box,
enter `http://localhost:10060/LITTLEBIGPLANETPS3_XML` in the second, and the output filename in the third. For this
guide I'll use `EBOOTlocalhost.elf`.
Now, copy the `EBOOTlocalhost.elf` file to where you got your `EBOOT.elf` file from, and you're now good to go.
To launch the game with the patched EBOOT, open up RPCS3, go to File, Boot SELF/ELF, and open up `EBOOTlocalhost.elf`.
Assuming you are running the patched version of RPCS3, you patched the file correctly, the database is migrated, and
Lighthouse is running, the game should now connect.
### LittleBigPlanet 1
For LittleBigPlanet 1 to work with RPCS3, follow the steps for LittleBigPlanet 2.
First, open your favourite hex editor. We recommend [HxD](https://mh-nexus.de/en/hxd/).
Once you have a hex editor open, open your `EBOOTlocalhost.elf` file and search for the hex
values `73 63 65 4E 70 43 6F 6D 6D 65 72 63 65 32`. In HxD, this would be done by clicking on Search -> Replace,
clicking on the `Hex-values` tab, and entering the hex there.
Then, you can zero it out by replacing it with `00 00 00 00 00 00 00 00 00 00 00 00 00 00`.
What this does is remove all the references to the sceNpCommerce2 function. The function is used for purchasing DLC,
which is impossible on Lighthouse. The reason why it must be patched out is because RPCS3 doesn't support the function
at this moment.
Then save the file, and your LBP1 EBOOT can now be used with RPCS3.
Finally, take a break. Chances are that took a while.
## Contributing Tips
### Database migrations
Some modifications may require updates to the database schema. You can automatically create a migration file by:
1. Making sure the tools are installed. You can do this by running `dotnet tool restore`.
2. Making sure `LIGHTHOUSE_DB_CONNECTION_STRING` is set correctly. See the `Running` section for more details.
3. Modifying the database schema via the C# portion of the code. Do not modify the actual SQL database.
4. Running `dotnet ef migrations add <NameOfMigrationInPascalCase> --project ProjectLighthouse`.
This process will create a migration file from the changes made in the C# code.
The new migrations will automatically be applied upon starting Lighthouse.
### Running tests
You can run tests either through your IDE or by running `dotnet tests`.
Keep in mind while running database tests (which most tests are) you need to have `LIGHTHOUSE_DB_CONNECTION_STRING` set.
### Continuous Integration (CI) Tips
- You can skip CI runs for a commit if you specify `[skip ci]` at the beginning of the commit name. This is useful for
formatting changes, etc.
- When creating your first pull request, CI will not run initially. A team member will have to approve you for use of
running CI on a pull request. This is because of GitHub policy.
## Compatibility across games and platforms ## Compatibility across games and platforms