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
gitUnpushed.txt
logs/*
npTicket*
# MSBuild stuff
bin/

View file

@ -6,6 +6,7 @@
<Path>.github</Path>
<Path>.gitignore</Path>
<Path>.idea</Path>
<Path>CONTRIBUTING.md</Path>
<Path>DatabaseMigrations</Path>
<Path>ProjectLighthouse.sln.DotSettings</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}"));
}
string stringContent = $"{LoginData.UsernamePrefix}{username}{number}{(char)0x00}";
//TODO: generate actual tickets
string stringContent = $"unitTestTicket{username}{number}";
HttpResponseMessage response = await this.Client.PostAsync
($"/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">
<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:Boolean x:Key="/Default/CodeEditing/TypingAssist/VirtualSpaceOnEnter/@EntryValue">True</s:Boolean>
<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.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]

View file

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

View file

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

View file

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

View file

@ -8,10 +8,12 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
using LBPUnion.ProjectLighthouse.Types.Tickets;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using IOFile = System.IO.File;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/login")]
@ -26,25 +28,29 @@ public class LoginController : ControllerBase
}
[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
{
loginData = LoginData.CreateFromString(body);
npTicket = NPTicket.CreateFromBytes(loginData);
}
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();
}
@ -60,11 +66,11 @@ public class LoginController : ControllerBase
// Get an existing token from the IP & username
GameToken? token = await this.database.GameTokens.Include
(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
{
token = await this.database.AuthenticateUser(loginData, ipAddress, titleId);
token = await this.database.AuthenticateUser(npTicket, ipAddress);
if (token == null)
{
Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance);
@ -110,7 +116,7 @@ public class LoginController : ControllerBase
GameTokenId = token.TokenId,
Timestamp = TimestampHelper.Timestamp,
IPAddress = ipAddress,
Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3
Platform = npTicket.Platform,
};
this.database.AuthenticationAttempts.Add(authAttempt);
@ -129,7 +135,7 @@ public class LoginController : ControllerBase
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.
// 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.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Matching;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]

View file

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

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -38,13 +38,13 @@ public class MessageController : ControllerBase
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
#else
(User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request);
(User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request);
if (userAndToken == null) return this.StatusCode(403, "");
if (userAndToken == null) return this.StatusCode(403, "");
// ReSharper disable once PossibleInvalidOperationException
User user = userAndToken.Value.Item1;
GameToken gameToken = userAndToken.Value.Item2;
// ReSharper disable once PossibleInvalidOperationException
User user = userAndToken.Value.Item1;
GameToken gameToken = userAndToken.Value.Item2;
#endif
string announceText = ServerSettings.Instance.AnnounceText;
@ -56,13 +56,13 @@ public class MessageController : ControllerBase
(
announceText +
#if DEBUG
"\n\n---DEBUG INFO---\n" +
$"user.UserId: {user.UserId}\n" +
$"token.Approved: {gameToken.Approved}\n" +
$"token.Used: {gameToken.Used}\n" +
$"token.UserLocation: {gameToken.UserLocation}\n" +
$"token.GameVersion: {gameToken.GameVersion}\n" +
"---DEBUG INFO---" +
"\n\n---DEBUG INFO---\n" +
$"user.UserId: {user.UserId}\n" +
$"token.Approved: {gameToken.Approved}\n" +
$"token.Used: {gameToken.Used}\n" +
$"token.UserLocation: {gameToken.UserLocation}\n" +
$"token.GameVersion: {gameToken.GameVersion}\n" +
"---DEBUG INFO---" +
#endif
"\n"
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -38,7 +38,7 @@ public class PublishController : ControllerBase
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 (string.IsNullOrEmpty(slot.RootLevel)) return this.BadRequest();
@ -79,7 +79,7 @@ public class PublishController : ControllerBase
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();
// Republish logic
@ -186,7 +186,7 @@ public class PublishController : ControllerBase
return this.Ok();
}
public async Task<Slot?> GetSlotFromBody()
private async Task<Slot?> getSlotFromBody()
{
this.Request.Body.Position = 0;
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.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -91,7 +91,7 @@ public class ReviewController : ControllerBase
if (user == null) return this.StatusCode(403, "");
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
Review? newReview = await this.GetReviewFromBody();
Review? newReview = await this.getReviewFromBody();
if (newReview == null) return this.BadRequest();
if (review == null)
@ -317,7 +317,7 @@ public class ReviewController : ControllerBase
return this.Ok();
}
public async Task<Review?> GetReviewFromBody()
private async Task<Review?> getReviewFromBody()
{
this.Request.Body.Position = 0;
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 Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -80,7 +80,7 @@ public class ScoreController : ControllerBase
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);
}
@ -99,11 +99,11 @@ public class ScoreController : ControllerBase
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")]
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
// 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.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi.Slots;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -177,7 +177,7 @@ public class SlotsController : ControllerBase
"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();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion)
IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable()
.OrderByDescending(s => s.Thumbsup)
.ThenBy(_ => rand.Next())
@ -279,14 +279,14 @@ public class SlotsController : ControllerBase
Random rand = new();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion)
IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable()
.OrderByDescending
(
// probably not the best way to do this?
s =>
{
return this.GetGameFilter(gameFilterType, token.GameVersion) switch
return this.getGameFilter(gameFilterType, token.GameVersion) switch
{
GameVersion.LittleBigPlanet1 => s.PlaysLBP1Unique,
GameVersion.LittleBigPlanet2 => s.PlaysLBP2Unique,
@ -337,7 +337,7 @@ public class SlotsController : ControllerBase
Random rand = new();
IEnumerable<Slot> slots = this.FilterByRequest(gameFilterType, dateFilterType, token.GameVersion)
IEnumerable<Slot> slots = this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion)
.AsEnumerable()
.OrderByDescending(s => s.Hearts)
.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.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 ?? "";
@ -392,7 +392,7 @@ public class SlotsController : ControllerBase
_ => 0,
};
GameVersion gameVersion = this.GetGameFilter(gameFilterType, version);
GameVersion gameVersion = this.getGameFilter(gameFilterType, version);
IQueryable<Slot> whereSlots;

View file

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

View file

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

View file

@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
@ -26,7 +26,7 @@ public class UserController : ControllerBase
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);
return user?.Serialize(gameVersion);
@ -38,7 +38,7 @@ public class UserController : ControllerBase
GameToken? token = await this.database.GameTokenFromRequest(this.Request);
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();
return this.Ok(user);
@ -51,7 +51,7 @@ public class UserController : ControllerBase
if (token == null) return this.StatusCode(403, "");
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);

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
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin;
@ -16,14 +13,4 @@ public class AdminPanelController : ControllerBase
{
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.Linq;
using System.Threading.Tasks;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Categories;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reviews;
using LBPUnion.ProjectLighthouse.Types.Settings;
using LBPUnion.ProjectLighthouse.Types.Tickets;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
@ -67,9 +66,9 @@ public class Database : DbContext
}
#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;
GameToken gameToken = new()
@ -78,15 +77,9 @@ public class Database : DbContext
User = user,
UserId = user.UserId,
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);
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 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)
{
byte[] result = binRdr.ReadBytes(byteCount);

View file

@ -99,6 +99,6 @@ public class GameVersionHelper
if (LittleBigPlanetVitaTitleIds.Contains(titleId)) return GameVersion.LittleBigPlanetVita;
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> 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();
}

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

View file

@ -15,12 +15,18 @@ public class AdminPanelPage : BaseLayout
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)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
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))
{
args ??= "";

View file

@ -11,9 +11,10 @@
<script>
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;
}
@ -43,7 +44,8 @@
<div class="field">
<label>Password</label>
<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>
</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();
string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool isQueued = false;
bool isHearted = false;
@ -27,23 +28,26 @@
string iconHash = Model.IconHash;
if (string.IsNullOrWhiteSpace(iconHash) || iconHash.StartsWith('g')) iconHash = ServerSettings.Instance.MissingIconHash;
}
<div style="display: flex; width: 100%;">
<div style="margin-right: 10px; background-image: url('/gameAssets/@iconHash'); height: 100px; width: 100px; background-size: cover; background-position: center; border-radius: 100%;">
<div class="card">
@{
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 style="height: fit-content; vertical-align: center; align-self: center">
<div class="cardStats">
@if (showLink)
{
<h2 style="margin-bottom: 2px;">
<h2>
<a href="~/slot/@Model.SlotId">@slotName</a>
</h2>
}
else
{
<h1 style="margin-bottom: 2px;">
<h1>
@slotName
</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="blue play icon" title="Plays"></i> <span>@Model.Plays</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>
</p>
</div>
<div style="height: 100px; margin-left: auto">
<div class="cardButtons">
<br>
@if (user != null)
{

View file

@ -3,12 +3,16 @@
@{
bool showLink = (bool?)ViewData["ShowLink"] ?? false;
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
}
<div style="display: flex;">
<div style="margin-right: 10px; background-image: url('/gameAssets/@Model.WebsiteAvatarHash'); height: 100px; width: 100px; background-size: cover; background-position: center; border-radius: .28571429rem;">
<div class="card">
@{
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 style="height: fit-content; vertical-align: center; align-self: center">
<div class="cardStats">
@if (showLink)
{
<h2 style="margin-bottom: 2px;">
@ -24,7 +28,7 @@
<p>
<i>@Model.Status</i>
</p>
<div class="statsUnderTitle">
<div class="cardStatsUnderTitle">
<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="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>
<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)
{
<div class="ui segment">
@ -18,10 +26,10 @@
@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)
@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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,25 @@ public class PhotosPage : BaseLayout
public int PhotoCount;
public List<Photo> Photos;
public string SearchValue;
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.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)}");
this.Photos = await this.Database.Photos.Include
(p => p.Creator)
this.Photos = await this.Database.Photos.Include(p => p.Creator)
.Where(p => p.Creator.Username.Contains(name) || p.PhotoSubjectCollection.Contains(name))
.OrderByDescending(p => p.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)

View file

@ -10,11 +10,12 @@
<script>
function onSubmit(form) {
const password = form['password'];
const confirmPassword = form['confirmPassword'];
password.value = sha256(password.value);
confirmPassword.value = sha256(confirmPassword.value);
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirmPassword");
const passwordSubmit = document.getElementById("password-submit");
const confirmPasswordSubmit = document.getElementById("confirm-submit");
passwordSubmit.value = sha256(passwordInput.value);
confirmPasswordSubmit.value = sha256(confirmPasswordInput.value);
return true;
}
@ -44,7 +45,8 @@
<div class="field">
<label>Password</label>
<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>
</div>
</div>
@ -52,7 +54,8 @@
<div class="field">
<label>Confirm Password</label>
<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>
</div>
</div>

View file

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

View file

@ -1,4 +1,5 @@
@page "/slots/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types.Levels
@model LBPUnion.ProjectLighthouse.Pages.SlotsPage
@ -9,8 +10,17 @@
<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)
{
bool isMobile = Model.Request.IsMobile();
<div class="ui segment">
@await Html.PartialAsync("Partials/SlotCardPartial", slot, new ViewDataDictionary(ViewData)
{
@ -23,16 +33,19 @@
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
</div>
}
@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)
@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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,26 @@ public class SlotsPage : BaseLayout
public int SlotCount;
public List<Slot> Slots;
public string SearchValue;
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.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)}");
this.Slots = await this.Database.Slots.Include
(p => p.Creator)
this.Slots = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Name.Contains(name))
.OrderByDescending(p => p.FirstUploaded)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)

View file

@ -1,6 +1,7 @@
@page "/user/{userId:int}"
@using System.IO
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types
@using LBPUnion.ProjectLighthouse.Types.Profiles
@model LBPUnion.ProjectLighthouse.Pages.UserPage
@ -43,6 +44,9 @@
{
"ShowLink", false
},
{
"IsMobile", Model.Request.IsMobile()
},
})
</div>
<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);
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
(p => p.Poster)
.Include(p => p.Target)
@ -43,4 +43,4 @@ public class UserPage : BaseLayout
return this.Page();
}
}
}

View file

@ -1,4 +1,5 @@
@page "/users/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Pages.UsersPage
@ -8,25 +9,37 @@
}
<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)
{
bool isMobile = Model.Request.IsMobile();
<div class="ui segment">
@await Html.PartialAsync("Partials/UserCardPartial", user, new ViewDataDictionary(ViewData)
{
{
"ShowLink", true
},
{
"IsMobile", isMobile
},
})
</div>
}
@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)
@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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
@ -22,20 +22,24 @@ public class UsersPage : BaseLayout
public List<User> Users;
public string SearchValue;
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.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)}");
this.Users = await this.Database.Users.Where
(u => !u.Banned)
this.Users = await this.Database.Users.Where(u => !u.Banned && u.Username.Contains(name))
.OrderByDescending(b => b.UserId)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)

View file

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

View file

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

View file

@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
@ -13,8 +15,11 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.OpenApi.Models;
#if RELEASE
using Microsoft.Extensions.Hosting.Internal;
#endif
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
services.AddSingleton<IHostLifetime, DebugWarmupLifetime>();
#else
@ -85,6 +114,15 @@ public class Startup
app.UseForwardedHeaders();
app.UseSwagger();
app.UseSwaggerUI
(
c =>
{
c.SwaggerEndpoint("v1/swagger.json", "Project Lighthouse API");
}
);
// Logs every request and the response to it
// Example: "200, 13ms: GET /LITTLEBIGPLANETPS3_XML/news"
// Example: "404, 127ms: GET /asdasd?query=osucookiezi727ppbluezenithtopplayhdhr"

View file

@ -8,14 +8,75 @@ div.main {
flex: 1;
}
div.statsUnderTitle > i {
#lighthouse-debug-info > p {
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.statsUnderTitle > span {
div.cardStatsUnderTitle > span {
margin-right: 5px;
}
#lighthouse-debug-info > p {
margin-bottom: 1px;
}
/*#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.Schema;
using System.Linq;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
@ -19,6 +20,7 @@ public class Slot
{
[XmlAttribute("type")]
[NotMapped]
[JsonIgnore]
public string Type { get; set; } = "user";
[Key]
@ -35,24 +37,29 @@ public class Slot
public string IconHash { get; set; } = "";
[XmlElement("rootLevel")]
[JsonIgnore]
public string RootLevel { get; set; } = "";
[JsonIgnore]
public string ResourceCollection { get; set; } = "";
[NotMapped]
[XmlElement("resource")]
[JsonIgnore]
public string[] Resources {
get => this.ResourceCollection.Split(",");
set => this.ResourceCollection = string.Join(',', value);
}
[XmlIgnore]
[JsonIgnore]
public int LocationId { get; set; }
[XmlIgnore]
public int CreatorId { get; set; }
[ForeignKey(nameof(CreatorId))]
[JsonIgnore]
public User? Creator { get; set; }
/// <summary>
@ -60,6 +67,7 @@ public class Slot
/// </summary>
[XmlElement("location")]
[ForeignKey(nameof(LocationId))]
[JsonIgnore]
public Location? Location { get; set; }
[XmlElement("initiallyLocked")]
@ -78,6 +86,7 @@ public class Slot
public string AuthorLabels { get; set; } = "";
[XmlElement("background")]
[JsonIgnore]
public string BackgroundHash { get; set; } = "";
[XmlElement("minPlayers")]
@ -103,6 +112,7 @@ public class Slot
[XmlIgnore]
[NotMapped]
[JsonIgnore]
public int Hearts {
get {
using Database database = new();
@ -124,42 +134,55 @@ public class Slot
public int PlaysComplete => this.PlaysLBP1Complete + this.PlaysLBP2Complete + this.PlaysLBP3Complete + this.PlaysLBPVitaComplete;
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP1 { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP1Complete { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP1Unique { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP2 { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP2Complete { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP2Unique { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP3 { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP3Complete { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBP3Unique { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBPVita { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBPVitaComplete { get; set; }
[XmlIgnore]
[JsonIgnore]
public int PlaysLBPVitaUnique { get; set; }
[NotMapped]
[JsonIgnore]
[XmlElement("thumbsup")]
public int Thumbsup {
get {
@ -170,6 +193,7 @@ public class Slot
}
[NotMapped]
[JsonIgnore]
[XmlElement("thumbsdown")]
public int Thumbsdown {
get {
@ -180,6 +204,7 @@ public class Slot
}
[NotMapped]
[JsonPropertyName("averageRating")]
[XmlElement("averageRating")]
public double RatingLBP1 {
get {
@ -193,6 +218,7 @@ public class Slot
}
[NotMapped]
[JsonIgnore]
[XmlElement("reviewCount")]
public int ReviewCount {
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,
RPCS3 = 1,
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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Profiles;
@ -13,11 +14,17 @@ public class User
public readonly ClientsConnected ClientsConnected = new();
public int UserId { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; }
public string IconHash { get; set; }
[JsonIgnore]
public int Game { get; set; }
[NotMapped]
[JsonIgnore]
public int Lists => 0;
/// <summary>
@ -26,6 +33,7 @@ public class User
public string Biography { get; set; }
[NotMapped]
[JsonIgnore]
public string WebsiteAvatarHash {
get {
string avatarHash = this.IconHash;
@ -40,6 +48,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int Reviews {
get {
using Database database = new();
@ -48,6 +57,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int Comments {
get {
using Database database = new();
@ -56,6 +66,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int PhotosByMe {
get {
using Database database = new();
@ -64,6 +75,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int PhotosWithMe {
get {
using Database database = new();
@ -71,15 +83,18 @@ public class User
}
}
[JsonIgnore]
public int LocationId { get; set; }
/// <summary>
/// The location of the profile card on the user's earth
/// </summary>
[ForeignKey("LocationId")]
[JsonIgnore]
public Location Location { get; set; }
[NotMapped]
[JsonIgnore]
public int HeartedLevels {
get {
using Database database = new();
@ -88,6 +103,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int HeartedUsers {
get {
using Database database = new();
@ -96,6 +112,7 @@ public class User
}
[NotMapped]
[JsonIgnore]
public int QueuedLevels {
get {
using Database database = new();
@ -103,10 +120,13 @@ public class User
}
}
[JsonIgnore]
public string Pins { get; set; } = "";
[JsonIgnore]
public string PlanetHash { get; set; } = "";
[JsonIgnore]
public int Hearts {
get {
using Database database = new();
@ -115,8 +135,10 @@ public class User
}
}
[JsonIgnore]
public bool IsAdmin { get; set; } = false;
[JsonIgnore]
public bool PasswordResetRequired { get; set; }
public string YayHash { get; set; } = "";
@ -125,6 +147,7 @@ public class User
#nullable enable
[NotMapped]
[JsonIgnore]
public string Status {
get {
using Database database = new();
@ -139,8 +162,10 @@ public class User
}
#nullable disable
[JsonIgnore]
public bool Banned { get; set; }
[JsonIgnore]
public string BannedReason { get; set; }
public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1)
@ -178,6 +203,7 @@ public class User
/// The number of used slots on the earth
/// </summary>
[NotMapped]
[JsonIgnore]
public int UsedSlots {
get {
using Database database = new();
@ -194,6 +220,7 @@ public class User
/// <summary>
/// The number of slots remaining on the earth
/// </summary>
[JsonIgnore]
public int FreeSlots => ServerSettings.Instance.EntitledSlots - this.UsedSlots;
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 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
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 [LBP Union Ministry of Technology Research and Development team](https://www.lbpunion.com/technology).
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)
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
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
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.
Please see `CONTRIBUTING.md` for more information.
## Compatibility across games and platforms