Merge branch 'LBPUnion:main' into main

This commit is contained in:
Michael Youngling 2024-06-15 20:21:14 -04:00 committed by GitHub
commit 0d5a2ea411
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 250 additions and 32 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.3",
"version": "8.0.6",
"commands": [
"dotnet-ef"
]

View file

@ -157,6 +157,13 @@ public class ReviewController : ControllerBase
List<GameReview> reviews = (await this.database.Reviews
.Where(r => r.SlotId == slotId)
.Select(r => new
{
Review = r,
SlotVersion = r.Slot!.GameVersion,
})
.Where(a => a.SlotVersion <= token.GameVersion)
.Select(a => a.Review)
.OrderByDescending(r => r.ThumbsUp - r.ThumbsDown)
.ThenByDescending(r => r.Timestamp)
.ApplyPagination(pageData)
@ -178,6 +185,13 @@ public class ReviewController : ControllerBase
List<GameReview> reviews = (await this.database.Reviews
.Where(r => r.ReviewerId == targetUserId)
.Select(r => new
{
Review = r,
SlotVersion = r.Slot!.GameVersion,
})
.Where(a => a.SlotVersion <= token.GameVersion)
.Select(a => a.Review)
.OrderByDescending(r => r.Timestamp)
.ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token));

View file

@ -1,8 +1,11 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Moderation.Cases;
using LBPUnion.ProjectLighthouse.Types.Users;
@ -26,7 +29,8 @@ public class AdminUserController : ControllerBase
/// Resets the user's earth decorations to a blank state. Useful for users who abuse audio for example.
/// </summary>
[HttpGet("wipePlanets")]
public async Task<IActionResult> WipePlanets([FromRoute] int id) {
public async Task<IActionResult> WipePlanets([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
@ -90,6 +94,78 @@ public class AdminUserController : ControllerBase
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Deletes every comment by the user. Useful in case of mass spam
/// </summary>
[HttpGet("wipeComments")]
public async Task<IActionResult> WipeComments([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
// Find every comment by the user, then set the deletion info on them
await this.database.Comments.Where(c => c.PosterUserId == targetedUser.UserId)
.ExecuteUpdateAsync(s =>
s.SetProperty(c => c.Deleted, true)
.SetProperty(c => c.DeletedBy, user.Username)
.SetProperty(c => c.DeletedType, "moderator"));
Logger.Success($"Deleted comments for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin);
await this.database.SendNotification(targetedUser.UserId,
"Your comments have been deleted by a moderator.");
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Deletes every score from the user. Useful in the case where a user cheated a ton of scores
/// </summary>
[HttpGet("wipeScores")]
public async Task<IActionResult> WipeScores([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
// Find and delete every score uploaded by the target user
await this.database.Scores.Where(c => c.UserId == targetedUser.UserId).ExecuteDeleteAsync();
Logger.Success($"Deleted scores for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin);
await this.database.SendNotification(targetedUser.UserId, "Your scores have been deleted by a moderator.");
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Forces the email verification of a user.
/// </summary>
[HttpGet("forceVerifyEmail")]
public async Task<IActionResult> ForceVerifyEmail([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
if (targetedUser.EmailAddress == null || targetedUser.EmailAddressVerified) return this.NotFound();
List<EmailVerificationTokenEntity> tokens = await this.database.EmailVerificationTokens
.Where(t => t.UserId == targetedUser.UserId)
.ToListAsync();
this.database.EmailVerificationTokens.RemoveRange(tokens);
targetedUser.EmailAddressVerified = true;
await this.database.SaveChangesAsync();
return this.Redirect($"/user/{targetedUser.UserId}");
}
[HttpPost("/admin/user/{id:int}/setPermissionLevel")]
public async Task<IActionResult> SetUserPermissionLevel([FromRoute] int id, [FromForm] PermissionLevel role)

View file

@ -324,6 +324,18 @@ else
<i class="trash alternate icon"></i>
<span>Wipe Earth Decorations</span>
</a>
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipeComments"
onclick="return confirm('Are you sure you want to delete ALL of this user\'s comments? This action cannot be reversed.')">
<i class="trash alternate icon"></i>
<span>Wipe User&apos;s Comments</span>
</a>
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipeScores"
onclick="return confirm('Are you sure you want to delete ALL of this user\'s scores? This action cannot be reversed.')">
<i class="trash alternate icon"></i>
<span>Wipe User&apos;s Scores</span>
</a>
@if (!Model.CommentsDisabledByModerator)
{
@ -333,6 +345,14 @@ else
</a>
}
@if (Model.ProfileUser.EmailAddress != null && !Model.ProfileUser.EmailAddressVerified)
{
<a class="ui green button" href="/moderation/user/@Model.ProfileUser.UserId/forceVerifyEmail">
<i class="check icon"></i>
<span>Forcibly Verify Email</span>
</a>
}
@if (Model.User.IsAdmin)
{
<div class="ui divider"></div>

View file

@ -11,6 +11,6 @@
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Localization\ProjectLighthouse.Localization.csproj" />
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="QRCoder" Version="1.5.1" />
</ItemGroup>
</Project>

View file

@ -9,14 +9,14 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers;
public class ReviewControllerTests
{
private static async Task InsertTestData(DatabaseContext database)
{
database.Slots.Add(new SlotEntity
{
SlotId = 1,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet3,
});
database.Slots.Add(new SlotEntity
{
SlotId = 2,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet2,
});
database.Reviews.Add(new ReviewEntity
{
ReviewId = 1,
ReviewerId = 1,
SlotId = 1,
});
database.Reviews.Add(new ReviewEntity
{
ReviewId = 2,
ReviewerId = 1,
SlotId = 2,
});
await database.SaveChangesAsync();
}
[Theory]
[InlineData(GameVersion.LittleBigPlanet2, 1)]
[InlineData(GameVersion.LittleBigPlanet3, 2)]
public async Task ReviewsBy_ShouldNotList_HigherGameVersions(GameVersion version, int expected)
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = version;
DatabaseContext database = await MockHelper.GetTestDatabase(new List<GameTokenEntity>
{
token,
});
await InsertTestData(database);
ReviewController controller = new(database);
controller.SetupTestController(token);
IActionResult response = await controller.ReviewsBy("unittest");
ReviewResponse review = response.CastTo<OkObjectResult, ReviewResponse>();
Assert.Equal(expected, review.Reviews.Count);
Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version));
}
[Theory]
[InlineData(GameVersion.LittleBigPlanet2, 2, 1)]
[InlineData(GameVersion.LittleBigPlanet2, 1, 0)]
[InlineData(GameVersion.LittleBigPlanet3, 2, 1)]
[InlineData(GameVersion.LittleBigPlanet3, 1, 1)]
public async Task ReviewsFor_ShouldNotList_HigherGameVersions(GameVersion version, int slotId, int expected)
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = version;
DatabaseContext database = await MockHelper.GetTestDatabase(new List<GameTokenEntity>
{
token,
});
await InsertTestData(database);
ReviewController controller = new(database);
controller.SetupTestController(token);
IActionResult response = await controller.ReviewsFor(slotId);
ReviewResponse review = response.CastTo<OkObjectResult, ReviewResponse>();
Assert.Equal(expected, review.Reviews.Count);
Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version));
}
}

View file

@ -9,16 +9,16 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.19.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="123.0.6312.8600" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.21.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -14,14 +14,14 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -30,7 +30,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
</ItemGroup>
<ItemGroup>

View file

@ -1,4 +1,6 @@
<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: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>
@ -96,10 +98,14 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Method/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8284009d_002De743_002D4d89_002D9402_002Da5bf9a89b657/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"&gt;&lt;ElementKinds&gt;&lt;Kind Name="METHOD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Affero/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=airfryer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ARRRRRRRRRR/@EntryIndexedValue">True</s:Boolean>

View file

@ -10,31 +10,31 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="GitInfo" Version="3.3.4">
<PackageReference Include="GitInfo" Version="3.3.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pfim" Version="0.11.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="Discord.Net.Webhook" Version="3.14.1" />
<PackageReference Include="Discord.Net.Webhook" Version="3.15.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Redis.OM" Version="0.6.1" />
<PackageReference Include="Redis.OM" Version="0.7.1" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="YamlDotNet" Version="15.1.2" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="YamlDotNet" Version="15.1.6" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="DistributedLock.MySql" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Localization\ProjectLighthouse.Localization.csproj" />
</ItemGroup>
</Project>

View file

@ -33,6 +33,9 @@ public struct SectionHeader
public SectionType Type;
public ushort Length;
public int Position;
public override string ToString() =>
$"SectionHeader(Type='{this.Type}', Length='{this.Length}', Position='{this.Position}')";
}
public class TicketReader : BinaryReader