From cf3aa29ba2c6f76e19b7388d4560e07849de8865 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:16:38 +0300 Subject: [PATCH] fix: address the 15 CodeRabbit findings from the wire-audit review Security: - roomcode PUT now requires instance ownership (any player could plant or relocate codes on foreign private instances) - /platformid/{accountId} requires auth (anonymous Steam/Oculus id enumeration oracle) - request tracing no longer captures successful /thread bodies by default (chat messages landed unredacted in logs); opt in via Trace__SuccessBodyPaths - anonymous action-link renewal clamped to 720h per consume Data integrity: - buyPurchaseReminder upserts its objective sentinel (second purchase threw UNIQUE after the wallet was debited) - progression reward gift + idempotency sentinel commit in one transaction (crash between them double-minted the reward) - ClubPermissionsGalleryAndModeration migration defaults corrected to the entity contract (CommentsJson '[]', ClubChatEnabled true); new one-shot ClubChatDefaultRepair migration fixes databases that applied the wrong defaults; bootstrap repairs '' CommentsJson rows - admin subroom-maxplayers audit entry commits with the mutation - account purge covers CustomAvatarItems (ownership deleted, created items reassigned to the system account) - duplicate photo reports ignored while one is unresolved Stability: - bootstrap only swallows the expected duplicate-column error; locked databases and schema failures now fail startup instead of the first endpoint - clone blob write failure degrades to an empty sub-room instead of a 500 after the clone row committed - resumed photo-feed cursor stamped DateTimeKind.Utc (Npgsql rejects Unspecified for timestamptz) - unreadcounts queries per requested room instead of materialising the whole comment keyspace Tests: - destroys-the-probe-session template guard is case-insensitive 74/74 tests pass. --- .../GameClientEndpointContractTests.cs | 7 +- .../API/Comments/CommentsController.cs | 32 +- .../API/Images/ImagesController.cs | 14 + .../API/Photos/PhotosController.cs | 4 +- .../ProgressionEventsController.cs | 6 + .../API/Rooms/V2/RoomsModerationController.cs | 16 +- .../API/Store/StorefrontsBuyController.cs | 27 +- .../Accounts/AccountsController.cs | 5 + .../Controllers/Admin/AdminController.cs | 4 +- .../Controllers/Link/LinkController.cs | 6 +- .../Controllers/Match/MatchController.cs | 19 +- ...658_ClubPermissionsGalleryAndModeration.cs | 10 +- ...01221530_ClubChatDefaultRepair.Designer.cs | 3654 +++++++++++++++++ .../20260801221530_ClubChatDefaultRepair.cs | 33 + DorkNet.Server/Services/PlayerAccountPurge.cs | 13 + DorkNet.Server/Startup/DatabaseBootstrap.cs | 20 +- .../Startup/RequestTracingExtensions.cs | 7 +- 17 files changed, 3842 insertions(+), 35 deletions(-) create mode 100644 DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.Designer.cs create mode 100644 DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.cs diff --git a/DorkNet.Server.Tests/GameClientEndpointContractTests.cs b/DorkNet.Server.Tests/GameClientEndpointContractTests.cs index 3cde1df..b6be662 100644 --- a/DorkNet.Server.Tests/GameClientEndpointContractTests.cs +++ b/DorkNet.Server.Tests/GameClientEndpointContractTests.cs @@ -66,8 +66,13 @@ public sealed class GameClientEndpointContractTests : if (!string.Equals(route.Method, "DELETE", StringComparison.OrdinalIgnoreCase)) return false; + // Case-insensitive to match the method comparison above: a route + // declared "Account/v1/me" would otherwise slip through and delete + // the probe player mid-run. var template = route.Template.Trim('/'); - return template is "account/me" or "account/v1/me" or "account/v2/me"; + return template.Equals("account/me", StringComparison.OrdinalIgnoreCase) + || template.Equals("account/v1/me", StringComparison.OrdinalIgnoreCase) + || template.Equals("account/v2/me", StringComparison.OrdinalIgnoreCase); } private static async Task ProbeAsync( diff --git a/DorkNet.Server/Controllers/API/Comments/CommentsController.cs b/DorkNet.Server/Controllers/API/Comments/CommentsController.cs index 54da8c8..0debc81 100644 --- a/DorkNet.Server/Controllers/API/Comments/CommentsController.cs +++ b/DorkNet.Server/Controllers/API/Comments/CommentsController.cs @@ -153,22 +153,30 @@ public class CommentsController(DorkNetDbContext db) : ControllerBase var me = Me; var lastRead = await LastReadAsync(me); - // Key + owner only: the payload column is irrelevant to counting. - var keys = await db.PlayerSettings - .Where(s => s.Key.StartsWith(CommentPrefix)) - .Select(s => new { s.Key, s.PlayerId }) - .ToListAsync(); - var counts = new Dictionary(); foreach (var id in roomIds) counts[id] = 0u; - foreach (var k in keys) + // One bounded query per requested room instead of materialising the + // entire comment keyspace: the room filter and the "your own notes + // are never unread" filter both run in SQL; only the numeric + // comment-id suffix still needs parsing here. Key + owner only: the + // payload column is irrelevant to counting. + foreach (var id in roomIds) { - if (!TrySplitCommentKey(k.Key, out var roomId, out var commentId)) continue; - if (!counts.ContainsKey(roomId)) continue; - if (k.PlayerId == me) continue; // your own notes are never unread - if (commentId <= lastRead.GetValueOrDefault(roomId)) continue; - counts[roomId]++; + var prefix = $"{CommentPrefix}{id}:"; + var keys = await db.PlayerSettings + .Where(s => s.Key.StartsWith(prefix) && s.PlayerId != me) + .Select(s => s.Key) + .ToListAsync(); + + var floor = lastRead.GetValueOrDefault(id); + foreach (var key in keys) + { + if (!TrySplitCommentKey(key, out var roomId, out var commentId)) continue; + if (roomId != id) continue; + if (commentId <= floor) continue; + counts[id]++; + } } var result = roomIds diff --git a/DorkNet.Server/Controllers/API/Images/ImagesController.cs b/DorkNet.Server/Controllers/API/Images/ImagesController.cs index 221d0ff..163490b 100644 --- a/DorkNet.Server/Controllers/API/Images/ImagesController.cs +++ b/DorkNet.Server/Controllers/API/Images/ImagesController.cs @@ -584,6 +584,20 @@ public class ImagesController( // ReportEntity has no TargetPhotoId column, so the photo id + blob name // ride in the message text the same way club reports carry "[club {id}]" // — a moderator needs the actual image to act on the report. + // + // One open report per (reporter, photo): the call is fire-and-forget + // and the client can't tell 200 from 404, so retries and re-opens are + // likely — same guard as the custom-avatar-item report path. + var marker = $"[photo {photo.Id} "; + var duplicate = await db.Reports.AnyAsync(r => + r.ReporterPlayerId == reporter && r.ResolvedAt == null && r.Message.StartsWith(marker)); + if (duplicate) + { + logger.LogInformation( + "[images] duplicate report of photo {PhotoId} by {Reporter} ignored", photo.Id, reporter); + return Ok(new RecNetResult { Success = true, Error = string.Empty }); + } + var message = $"[photo {photo.Id} {photo.BlobName}] reported from the in-game photo viewer"; db.Reports.Add(new ReportEntity { diff --git a/DorkNet.Server/Controllers/API/Photos/PhotosController.cs b/DorkNet.Server/Controllers/API/Photos/PhotosController.cs index d42e86b..bac9501 100644 --- a/DorkNet.Server/Controllers/API/Photos/PhotosController.cs +++ b/DorkNet.Server/Controllers/API/Photos/PhotosController.cs @@ -401,8 +401,10 @@ public class PhotosController( itemIds.AddRange(injectedIds.Where(pinned.Contains)); } + // Utc kind explicitly: the ticks came from a UtcNow-derived CreatedAt, + // and Npgsql refuses to write an Unspecified DateTime to timestamptz. DateTime? cutoffAt = resumed && cursorTicks > 0 && cursorTicks <= DateTime.MaxValue.Ticks - ? new DateTime(cursorTicks) + ? new DateTime(cursorTicks, DateTimeKind.Utc) : (DateTime?)null; var cutoffId = resumed ? cursorId : 0L; if (phase < 0) phase = 0; diff --git a/DorkNet.Server/Controllers/API/ProgressionEvents/ProgressionEventsController.cs b/DorkNet.Server/Controllers/API/ProgressionEvents/ProgressionEventsController.cs index eacce1d..f385c74 100644 --- a/DorkNet.Server/Controllers/API/ProgressionEvents/ProgressionEventsController.cs +++ b/DorkNet.Server/Controllers/API/ProgressionEvents/ProgressionEventsController.cs @@ -226,6 +226,11 @@ public class ProgressionEventsController( IsValid = true, SupportsCurrentPlatform = true, }; + // The sentinel key needs the generated gift id, so this takes two + // saves — but they must commit together: a crash between them would + // leave a gift with no sentinel, and the next Collect would mint the + // reward a second time. + await using var tx = await db.Database.BeginTransactionAsync(); db.GiftPackages.Add(gift); await db.SaveChangesAsync(); @@ -237,6 +242,7 @@ public class ProgressionEventsController( ClearedAt = DateTime.UtcNow, }); await db.SaveChangesAsync(); + await tx.CommitAsync(); return Ok(global::DorkNet.Server.Controllers.API.Avatar.V2.AvatarGiftsController.ToWire(gift)); } diff --git a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs index 775dfb4..a0bbbe4 100644 --- a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs +++ b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs @@ -1642,7 +1642,21 @@ public class RoomsModerationController( var newBlob = CloneBlobName(cloneId, orderIndex); var (dstBucket, dstKey) = BlobRouter.Route(newBlob); - await storage.PutAsync(dstBucket, dstKey, bytes, "application/octet-stream"); + try + { + await storage.PutAsync(dstBucket, dstKey, bytes, "application/octet-stream"); + } + catch (Exception ex) + { + // Same reasoning as the read side: the clone row is already + // committed, so a storage outage must degrade to an empty + // sub-room, not a 500 that leaves a failed-looking clone still + // pointing at the source's data. + logger.LogWarning(ex, + "[room-clone] could not write blob {Blob}; room {Room} sub-room {Sub} starts empty", + newBlob, cloneId, orderIndex); + return null; + } db.RoomDataBlobs.Add(new RoomDataBlobEntity { diff --git a/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs b/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs index 01196e4..17bfd51 100644 --- a/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs +++ b/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs @@ -654,13 +654,28 @@ public class StorefrontsBuyController( $"buyPurchaseReminder:{item.Slug}"); var giftId = await GrantWithGiftBoxAsync(pid, item, currencyType); - db.ObjectiveProgress.Add(new ObjectiveProgressEntity + // Upsert: (PlayerId, Key) is unique and the same reminder can be + // bought again — a blind Add threw "UNIQUE constraint failed" and + // turned the second purchase into a 500 AFTER the currency had + // already been taken and the gift saved. + var progressKey = $"purchaseReminder:{req.PurchaseReminderId}"; + var progress = await db.ObjectiveProgress + .FirstOrDefaultAsync(o => o.PlayerId == pid && o.Key == progressKey); + if (progress is null) { - PlayerId = pid, - Key = $"purchaseReminder:{req.PurchaseReminderId}", - IsCompleted = true, - ClearedAt = DateTime.UtcNow, - }); + db.ObjectiveProgress.Add(new ObjectiveProgressEntity + { + PlayerId = pid, + Key = progressKey, + IsCompleted = true, + ClearedAt = DateTime.UtcNow, + }); + } + else + { + progress.IsCompleted = true; + progress.ClearedAt = DateTime.UtcNow; + } await db.SaveChangesAsync(); return Ok(PurchaseReminderResponse(newBalance, currencyType, item.Slug, giftId)); diff --git a/DorkNet.Server/Controllers/Accounts/AccountsController.cs b/DorkNet.Server/Controllers/Accounts/AccountsController.cs index 96970fc..c01a1d4 100644 --- a/DorkNet.Server/Controllers/Accounts/AccountsController.cs +++ b/DorkNet.Server/Controllers/Accounts/AccountsController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DorkNet.Models.Auth; @@ -808,8 +809,12 @@ public class AccountsController( /// Unknown accounts get an empty string — "no platform identity" is the /// honest answer and keeps the promise resolved rather than rejected. /// + // LastPlatformId is a third-party identifier (Steam/Oculus id); without + // an auth gate this was an anonymous enumeration oracle over every + // account. The 2023 client only ever calls it with a token. [HttpGet("/platformid/{accountId:long}")] [HttpGet("/account/v1/{accountId:long}/platformid")] + [Authorize] public async Task GetPlatformId(long accountId) { var platformId = await db.Players diff --git a/DorkNet.Server/Controllers/Admin/AdminController.cs b/DorkNet.Server/Controllers/Admin/AdminController.cs index 40a8195..953e8a9 100644 --- a/DorkNet.Server/Controllers/Admin/AdminController.cs +++ b/DorkNet.Server/Controllers/Admin/AdminController.cs @@ -1158,10 +1158,12 @@ public class AdminController( var previous = scene.MaxPlayers; scene.MaxPlayers = Math.Clamp(body.MaxPlayers, 1, 80); scene.DataModifiedAt = DateTime.UtcNow; - await db.SaveChangesAsync(); + // LogAsync only stages the AdminActionEntity; save afterwards so the + // capacity change and its audit record commit together. await LogAsync("update_subroom_maxplayers", "room", id, $"subRoom={subRoomId} maxPlayers {previous} -> {scene.MaxPlayers}"); + await db.SaveChangesAsync(); return Ok(new { diff --git a/DorkNet.Server/Controllers/Link/LinkController.cs b/DorkNet.Server/Controllers/Link/LinkController.cs index 47ba3c5..fe73b9b 100644 --- a/DorkNet.Server/Controllers/Link/LinkController.cs +++ b/DorkNet.Server/Controllers/Link/LinkController.cs @@ -223,7 +223,11 @@ public class LinkController(DorkNetDbContext db, ILogger logger) payload.Uses++; // AutoRenewHours present ⇒ the code renews rather than ages out. - if (req.ValidHours is > 0) payload.ExpiresAt = DateTime.UtcNow.AddHours(req.ValidHours.Value); + // Clamped: this route is anonymous and the value is client-supplied, + // so an unbounded renewal would let anyone holding the code extend + // its life indefinitely past what the creator chose. + if (req.ValidHours is > 0) + payload.ExpiresAt = DateTime.UtcNow.AddHours(Math.Min(req.ValidHours.Value, 720)); var encoded = JsonSerializer.Serialize(payload); if (encoded.Length <= ValueLimit) row.Value = encoded; diff --git a/DorkNet.Server/Controllers/Match/MatchController.cs b/DorkNet.Server/Controllers/Match/MatchController.cs index f651bc0..e06d8a2 100644 --- a/DorkNet.Server/Controllers/Match/MatchController.cs +++ b/DorkNet.Server/Controllers/Match/MatchController.cs @@ -396,14 +396,19 @@ public class MatchController( .Select(p => new { p.Id, p.OwnerPlayerId }) .FirstOrDefaultAsync(); - // forceChange only lets a host reclaim a code from one of their OWN - // instances; another player's code is never stolen. On refusal the - // client just re-reads whatever code we echo back, so we fall - // through to the read path instead of erroring. - var blocked = clash is not null - && (clash.OwnerPlayerId != pid || forceChange != true); - var row = await db.PrivateInstances.FirstOrDefaultAsync(p => p.Id == instanceId); + + // Only the instance's owner may set its code (same guard as + // MarkPrivate), otherwise any player could plant or relocate a + // code onto someone else's instance. forceChange only lets a host + // reclaim a code from one of their OWN instances; another + // player's code is never stolen. On refusal the client just + // re-reads whatever code we echo back, so we fall through to the + // read path instead of erroring. + var blocked = row is null || row.OwnerPlayerId != pid + || (clash is not null + && (clash.OwnerPlayerId != pid || forceChange != true)); + if (!blocked && row is not null) { if (clash is not null) diff --git a/DorkNet.Server/Migrations/20260728061658_ClubPermissionsGalleryAndModeration.cs b/DorkNet.Server/Migrations/20260728061658_ClubPermissionsGalleryAndModeration.cs index c8f0987..59577ee 100644 --- a/DorkNet.Server/Migrations/20260728061658_ClubPermissionsGalleryAndModeration.cs +++ b/DorkNet.Server/Migrations/20260728061658_ClubPermissionsGalleryAndModeration.cs @@ -11,19 +11,25 @@ namespace DorkNet.Server.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + // Defaults must match the entity contract (TestCaseEntity inits + // CommentsJson to "[]", ClubEntity inits ClubChatEnabled to true): + // existing rows get the column default, and "" is not valid JSON + // while false silently muted every pre-existing club's chat. The + // ClubChatDefaultRepair migration fixes databases that applied + // this migration before the defaults were corrected. migrationBuilder.AddColumn( name: "CommentsJson", table: "TestCases", type: "TEXT", nullable: false, - defaultValue: ""); + defaultValue: "[]"); migrationBuilder.AddColumn( name: "ClubChatEnabled", table: "Clubs", type: "INTEGER", nullable: false, - defaultValue: false); + defaultValue: true); migrationBuilder.AddColumn( name: "MinLevel", diff --git a/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.Designer.cs b/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.Designer.cs new file mode 100644 index 0000000..8b38d32 --- /dev/null +++ b/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.Designer.cs @@ -0,0 +1,3654 @@ +// +using System; +using DorkNet.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DorkNet.Server.Migrations +{ + [DbContext(typeof(DorkNetDbContext))] + [Migration("20260801221530_ClubChatDefaultRepair")] + partial class ClubChatDefaultRepair + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.AdminActionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AdminPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("TargetId") + .HasColumnType("INTEGER"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminPlayerId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("TargetType", "TargetId"); + + b.ToTable("AdminActions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.AvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomAvatarItemsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EquippedItemsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FaceFeatures") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("InventoryJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OutfitSelections") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("OutfitSelectionsV2") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("SavedOutfitsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("Avatars"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.BugReportEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ClientVersion") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("GameSessionId") + .HasColumnType("INTEGER"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ReadAt") + .HasColumnType("TEXT"); + + b.Property("ReporterPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ReadAt"); + + b.HasIndex("ReporterPlayerId"); + + b.ToTable("BugReports"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("PlayerId"); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CharadesWordListEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsBuiltIn") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WordsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CharadesWordLists"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ModerationState") + .HasColumnType("INTEGER"); + + b.Property("SenderPlayerId") + .HasColumnType("INTEGER"); + + b.Property("SentAt") + .HasColumnType("TEXT"); + + b.Property("ThreadKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ThreadKey", "SentAt"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatThreadEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ThreadKey") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ThreadKey") + .IsUnique(); + + b.ToTable("ChatThreads"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ChatThreadMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JoinedAt") + .HasColumnType("TEXT"); + + b.Property("LastReadMessageId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("SnoozeUntil") + .HasColumnType("TEXT"); + + b.Property("ThreadKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("ThreadKey", "PlayerId") + .IsUnique(); + + b.ToTable("ChatThreadMembers"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CheerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CheeredAt") + .HasColumnType("TEXT"); + + b.Property("FromPlayerId") + .HasColumnType("INTEGER"); + + b.Property("TargetInventionId") + .HasColumnType("INTEGER"); + + b.Property("TargetPhotoId") + .HasColumnType("INTEGER"); + + b.Property("TargetPlayerId") + .HasColumnType("INTEGER"); + + b.Property("TargetRoomId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TargetPhotoId"); + + b.HasIndex("TargetPlayerId"); + + b.HasIndex("TargetRoomId"); + + b.HasIndex("FromPlayerId", "TargetPlayerId", "TargetRoomId", "TargetPhotoId", "Type") + .IsUnique(); + + b.ToTable("Cheers"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAdditionalImageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClubId", "Slot") + .IsUnique(); + + b.ToTable("ClubAdditionalImages"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAnnouncementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClubId", "CreatedAt"); + + b.ToTable("ClubAnnouncements"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubAnnouncementReadEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AnnouncementId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("ReadAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("AnnouncementId", "PlayerId") + .IsUnique(); + + b.ToTable("ClubAnnouncementReads"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubCategoryAssignmentEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryTagId") + .HasColumnType("INTEGER"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryTagId"); + + b.HasIndex("ClubId", "CategoryTagId") + .IsUnique(); + + b.ToTable("ClubCategoryAssignments"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubCategoryTagEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("OrderIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("ClubCategoryTags"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowJuniors") + .HasColumnType("INTEGER"); + + b.Property("BanStatus") + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ClubChatEnabled") + .HasColumnType("INTEGER"); + + b.Property("ClubType") + .HasColumnType("INTEGER"); + + b.Property("ClubhouseRoomId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsRRO") + .HasColumnType("INTEGER"); + + b.Property("Joinability") + .HasColumnType("INTEGER"); + + b.Property("MinLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubMembershipEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("JoinedAt") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("ClubId", "PlayerId") + .IsUnique(); + + b.ToTable("ClubMemberships"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubRolePermissionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApproveMember") + .HasColumnType("INTEGER"); + + b.Property("BanUnban") + .HasColumnType("INTEGER"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("CreateEvent") + .HasColumnType("INTEGER"); + + b.Property("EditDetails") + .HasColumnType("INTEGER"); + + b.Property("EditPermissionSettings") + .HasColumnType("INTEGER"); + + b.Property("MembershipType") + .HasColumnType("INTEGER"); + + b.Property("PostAnnouncement") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClubId", "MembershipType") + .IsUnique(); + + b.ToTable("ClubRolePermissions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ClubSubscriptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("SubscribedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClubId"); + + b.HasIndex("PlayerId", "ClubId") + .IsUnique(); + + b.ToTable("ClubSubscriptions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CohortAssignmentEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.Property("CohortKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Variant") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "CohortKey") + .IsUnique(); + + b.ToTable("CohortAssignments"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CommunityBoardEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Json") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CommunityBoardRows"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CouponEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MaxRedemptions") + .HasColumnType("INTEGER"); + + b.Property("RedemptionCount") + .HasColumnType("INTEGER"); + + b.Property("RewardAmount") + .HasColumnType("INTEGER"); + + b.Property("RewardCurrencyType") + .HasColumnType("INTEGER"); + + b.Property("RewardItemSlug") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RewardType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CouponRedemptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RedeemedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CouponId", "PlayerId") + .IsUnique(); + + b.ToTable("CouponRedemptions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CurrencyBalanceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("INTEGER"); + + b.Property("CurrencyType") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "CurrencyType") + .IsUnique(); + + b.ToTable("CurrencyBalances"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CustomAvatarItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssetName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BaseAvatarItemId") + .HasColumnType("INTEGER"); + + b.Property("CheerCount") + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsFeatured") + .HasColumnType("INTEGER"); + + b.Property("IsPublic") + .HasColumnType("INTEGER"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("PurchaseCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("IsFeatured"); + + b.HasIndex("IsPublic"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.ToTable("CustomAvatarItems"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.CustomAvatarItemOwnershipEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AcquiredAt") + .HasColumnType("TEXT"); + + b.Property("CustomAvatarItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CustomAvatarItemId"); + + b.HasIndex("PlayerId", "CustomAvatarItemId") + .IsUnique(); + + b.ToTable("CustomAvatarItemOwnership"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.DormStateEntity", b => + { + b.Property("PlayerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrentDataBlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("PlayerId"); + + b.ToTable("DormStates"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.GameRewardSelectionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("GiftContext") + .HasColumnType("INTEGER"); + + b.Property("GrantedItemId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Offer1ItemId") + .HasColumnType("INTEGER"); + + b.Property("Offer2ItemId") + .HasColumnType("INTEGER"); + + b.Property("Offer3ItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RewardType") + .HasColumnType("INTEGER"); + + b.Property("SelectedAt") + .HasColumnType("TEXT"); + + b.Property("SelectedGiftDropId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "SelectedAt"); + + b.ToTable("GameRewardSelections"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.GameSessionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityLevelId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("MaxCapacity") + .HasColumnType("INTEGER"); + + b.Property("PhotonRoomName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PlayerCount") + .HasColumnType("INTEGER"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("RoomId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "Region"); + + b.ToTable("GameSessions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.GiftPackageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvatarItemDescOrHairDyeDesc") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("AvatarItemType") + .HasColumnType("INTEGER"); + + b.Property("ConsumableItemDesc") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Consumed") + .HasColumnType("INTEGER"); + + b.Property("ConsumedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasColumnType("INTEGER"); + + b.Property("CurrencyType") + .HasColumnType("INTEGER"); + + b.Property("EquipmentModificationGuid") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("EquipmentPrefabName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("FromPlayerId") + .HasColumnType("INTEGER"); + + b.Property("GiftContext") + .HasColumnType("INTEGER"); + + b.Property("GiftRarity") + .HasColumnType("INTEGER"); + + b.Property("IsGifted") + .HasColumnType("INTEGER"); + + b.Property("IsValid") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("PackageMaterial") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PackageVariant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.Property("RecipientPlayerId") + .HasColumnType("INTEGER"); + + b.Property("SourceStoreItemId") + .HasColumnType("INTEGER"); + + b.Property("SupportsCurrentPlatform") + .HasColumnType("INTEGER"); + + b.Property("Xp") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RecipientPlayerId", "Consumed"); + + b.ToTable("GiftPackages"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.InventionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CheerCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreationRoomId") + .HasColumnType("INTEGER"); + + b.Property("CreatorPermission") + .HasColumnType("INTEGER"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CurrentBlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentVersionNumber") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("FirstPublishedAt") + .HasColumnType("TEXT"); + + b.Property("GeneralPermission") + .HasColumnType("INTEGER"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAgInvention") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsPublished") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NumPlayersHaveUsedInRoom") + .HasColumnType("INTEGER"); + + b.Property("Permission") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("ReplicationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SpawnCount") + .HasColumnType("INTEGER"); + + b.Property("TagsCsv") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CheerCount"); + + b.HasIndex("CreatorPlayerId"); + + b.ToTable("Inventions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.InventionVersionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("InstantiationCost") + .HasColumnType("INTEGER"); + + b.Property("InventionId") + .HasColumnType("INTEGER"); + + b.Property("LightsCost") + .HasColumnType("INTEGER"); + + b.Property("ReplicationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("VersionNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InventionId", "VersionNumber") + .IsUnique(); + + b.ToTable("InventionVersions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.IpBanEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BannedAt") + .HasColumnType("TEXT"); + + b.Property("BannedByAdminId") + .HasColumnType("INTEGER"); + + b.Property("Cidr") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Until") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Cidr"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ItemWishlistEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ItemKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "ItemKey", "ItemType") + .IsUnique(); + + b.ToTable("ItemWishlists"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.KeepsakeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("EarnedAt") + .HasColumnType("TEXT"); + + b.Property("EventKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("PlayerId", "EventKey"); + + b.ToTable("Keepsakes"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.LeaderboardChannelMetaEntity", b => + { + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("Channel") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LowerIsBetter") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ValueFormat") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("RoomId", "Channel"); + + b.HasIndex("RoomId"); + + b.ToTable("LeaderboardChannelMeta"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.LeaderboardStatEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("StatChannel") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "PlayerId", "StatChannel") + .IsUnique(); + + b.HasIndex("RoomId", "StatChannel", "Value"); + + b.ToTable("LeaderboardStats"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.LoadingScreenTipEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Context") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("PlatformMask") + .HasColumnType("INTEGER"); + + b.Property("RoomNamesCsv") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LoadingScreenTips"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.MessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ReadAt") + .HasColumnType("TEXT"); + + b.Property("RecipientPlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("SenderPlayerId") + .HasColumnType("INTEGER"); + + b.Property("SentAt") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RecipientPlayerId", "SentAt"); + + b.HasIndex("SenderPlayerId", "SentAt"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.NotificationPrefsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAll") + .HasColumnType("INTEGER"); + + b.Property("AllowAnnouncements") + .HasColumnType("INTEGER"); + + b.Property("AllowEventInvite") + .HasColumnType("INTEGER"); + + b.Property("AllowFriendRequest") + .HasColumnType("INTEGER"); + + b.Property("AllowMessage") + .HasColumnType("INTEGER"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Platform") + .IsUnique(); + + b.ToTable("NotificationPrefs"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ObjectiveProgressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClearedAt") + .HasColumnType("TEXT"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Key") + .IsUnique(); + + b.ToTable("ObjectiveProgress"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PendingDeviceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstSeenAt") + .HasColumnType("TEXT"); + + b.Property("LastIp") + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.Property("PlatformId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PendingDevices"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PhotoEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlobName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Caption") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("CheerCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPublic") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("TaggedPlayerIdsCsv") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("UploaderPlayerId") + .HasColumnType("INTEGER"); + + b.Property("ViewCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlobName"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RoomId"); + + b.HasIndex("UploaderPlayerId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlatformIgnoreEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.Property("PlatformUserId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Platform", "PlatformUserId") + .IsUnique(); + + b.ToTable("PlatformIgnores"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerDeviceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("FirstSeen") + .HasColumnType("TEXT"); + + b.Property("IsBanned") + .HasColumnType("INTEGER"); + + b.Property("LastSeen") + .HasColumnType("TEXT"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerDevices"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEloEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Elo") + .HasColumnType("INTEGER"); + + b.Property("GameMode") + .HasColumnType("INTEGER"); + + b.Property("Losses") + .HasColumnType("INTEGER"); + + b.Property("MatchesPlayed") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Wins") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GameMode", "Elo"); + + b.HasIndex("PlayerId", "GameMode") + .IsUnique(); + + b.ToTable("PlayerElo"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BannedUntil") + .HasColumnType("TEXT"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Birthday") + .HasColumnType("TEXT"); + + b.Property("CanReceiveInvites") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedFromDeviceId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("IsCommunityTeam") + .HasColumnType("INTEGER"); + + b.Property("IsDeveloper") + .HasColumnType("INTEGER"); + + b.Property("IsJunior") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastIp") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastPlatform") + .HasColumnType("INTEGER"); + + b.Property("LastPlatformId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ProfileImageName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Reputation") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("XP") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEventEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Capacity") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EndsAt") + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("StartsAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("StartsAt"); + + b.ToTable("PlayerEvents"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEventResponseEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Response") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("EventId", "PlayerId") + .IsUnique(); + + b.ToTable("PlayerEventResponses"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerInventoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AcquiredAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("ItemSlug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("PlayerId", "ItemSlug") + .IsUnique(); + + b.ToTable("PlayerInventory"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerSettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId", "Key") + .IsUnique(); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Accessibility") + .HasColumnType("INTEGER"); + + b.Property("CheerCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CustomWarning") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsCurated") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("OrderIndex") + .HasColumnType("INTEGER"); + + b.Property("SupportsJuniors") + .HasColumnType("INTEGER"); + + b.Property("SupportsLevelVoting") + .HasColumnType("INTEGER"); + + b.Property("SupportsScreens") + .HasColumnType("INTEGER"); + + b.Property("SupportsTeleportVR") + .HasColumnType("INTEGER"); + + b.Property("SupportsWalkVR") + .HasColumnType("INTEGER"); + + b.Property("TagsCsv") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("VisitCount") + .HasColumnType("INTEGER"); + + b.Property("VisitorCount") + .HasColumnType("INTEGER"); + + b.Property("WarningMask") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("IsCurated"); + + b.HasIndex("Name"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistInteractionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Cheered") + .HasColumnType("INTEGER"); + + b.Property("Favorited") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("PlaylistId", "PlayerId") + .IsUnique(); + + b.ToTable("PlaylistInteractions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlaylistRoomEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("OrderIndex") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("RoomId"); + + b.HasIndex("PlaylistId", "OrderIndex"); + + b.ToTable("PlaylistRooms"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PrivateInstanceEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DataBlob") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaxCapacity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("OwnerPlayerId") + .HasColumnType("INTEGER"); + + b.Property("PhotonRegion") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("PhotonRoomId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RoomCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("SubRoomId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerPlayerId"); + + b.HasIndex("RoomId", "SubRoomId"); + + b.ToTable("PrivateInstances"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PrivateInstanceInviteeEntity", b => + { + b.Property("PrivateInstanceId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("InvitedAt") + .HasColumnType("TEXT"); + + b.Property("LatestInviteMessageId") + .HasColumnType("INTEGER"); + + b.HasKey("PrivateInstanceId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PrivateInstanceInvitees"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PushTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Token"); + + b.HasIndex("PlayerId", "Platform") + .IsUnique(); + + b.ToTable("PushTokens"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RelationshipEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Favorited") + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("Muted") + .HasColumnType("INTEGER"); + + b.Property("RequesterId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TargetId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RequesterId", "TargetId") + .IsUnique(); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ReportEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("GameSessionId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("ReporterPlayerId") + .HasColumnType("INTEGER"); + + b.Property("ResolutionNote") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT"); + + b.Property("ResolverAdminId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("TargetEventId") + .HasColumnType("INTEGER"); + + b.Property("TargetInventionId") + .HasColumnType("INTEGER"); + + b.Property("TargetPlayerId") + .HasColumnType("INTEGER"); + + b.Property("TargetRoomId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ReporterPlayerId"); + + b.HasIndex("ResolvedAt"); + + b.HasIndex("TargetPlayerId"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomBanEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BanType") + .HasColumnType("INTEGER"); + + b.Property("BannedByPlayerId") + .HasColumnType("INTEGER"); + + b.Property("BannedPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("Until") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BannedPlayerId"); + + b.HasIndex("RoomId", "BannedPlayerId"); + + b.ToTable("RoomBans"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomBookmarkEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("PlayerId", "RoomId") + .IsUnique(); + + b.ToTable("RoomBookmarks"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomConsumableEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CurrencyId") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("RoomConsumables"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomConsumableOwnershipEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyCode") + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ModifiedAt") + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomConsumableId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoomConsumableId"); + + b.HasIndex("PlayerId", "RoomConsumableId") + .IsUnique(); + + b.ToTable("RoomConsumableOwnership"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyBalanceEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomCurrencyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoomCurrencyId"); + + b.HasIndex("PlayerId", "RoomCurrencyId") + .IsUnique(); + + b.ToTable("RoomCurrencyBalances"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("RoomCurrencies"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomCurrencyPurchaseOfferEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrencyType") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RoomCurrencyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("RoomCurrencyId"); + + b.ToTable("RoomCurrencyPurchaseOffers"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomDataBlobEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Bytes") + .HasColumnType("BLOB"); + + b.Property("ReferencedFilenamesCsv") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("SubRoomId") + .HasColumnType("INTEGER"); + + b.Property("UploadedAt") + .HasColumnType("TEXT"); + + b.Property("UploadedByPlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlobName") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("RoomDataBlobs"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Accessibility") + .HasColumnType("INTEGER"); + + b.Property("AllowNewUsers") + .HasColumnType("INTEGER"); + + b.Property("AllowsJuniors") + .HasColumnType("INTEGER"); + + b.Property("CheerCount") + .HasColumnType("INTEGER"); + + b.Property("CloningAllowed") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CurrentDataBlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CustomRoomWarning") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DisableMicAutoMute") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("HiddenFromBrowse") + .HasColumnType("INTEGER"); + + b.Property("HotScore") + .HasColumnType("REAL"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAGRoom") + .HasColumnType("INTEGER"); + + b.Property("IsDormRoom") + .HasColumnType("INTEGER"); + + b.Property("IsRoomLinkedToRecRoomStudio") + .HasColumnType("INTEGER"); + + b.Property("IsStudioRoom") + .HasColumnType("INTEGER"); + + b.Property("LoadScreensJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("LocationReplicationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaxCapacity") + .HasColumnType("INTEGER"); + + b.Property("MaxPlayerCalculationMode") + .HasColumnType("INTEGER"); + + b.Property("MinLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("PromoExternalContentJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("PromoImagesJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("RoomWarningMask") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("StudioSessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SupportsLevelVoting") + .HasColumnType("INTEGER"); + + b.Property("SupportsMobile") + .HasColumnType("INTEGER"); + + b.Property("SupportsScreens") + .HasColumnType("INTEGER"); + + b.Property("SupportsTeleportVR") + .HasColumnType("INTEGER"); + + b.Property("SupportsVRLow") + .HasColumnType("INTEGER"); + + b.Property("SupportsWalkVR") + .HasColumnType("INTEGER"); + + b.Property("TagsCsv") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("VisitCount") + .HasColumnType("INTEGER"); + + b.Property("VisitorCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("HotScore"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomKeyEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(174) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("ReplicationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("RoomId"); + + b.HasIndex("RoomId", "Name"); + + b.ToTable("RoomKeys"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomKeyPurchaseEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PaidPrice") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("PurchasedAt") + .HasColumnType("TEXT"); + + b.Property("RoomKeyId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("RoomKeyId", "PlayerId") + .IsUnique(); + + b.ToTable("RoomKeyPurchases"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomRoleEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Accepted") + .HasColumnType("INTEGER"); + + b.Property("GrantedAt") + .HasColumnType("TEXT"); + + b.Property("GrantedByPlayerId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.HasIndex("RoomId", "PlayerId", "Role") + .IsUnique(); + + b.ToTable("RoomRoles"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomSceneEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanMatchmakeInto") + .HasColumnType("INTEGER"); + + b.Property("DataBlobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DataModifiedAt") + .HasColumnType("TEXT"); + + b.Property("IsSandbox") + .HasColumnType("INTEGER"); + + b.Property("MaxPlayers") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("OrderIndex") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("RoomSceneLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StudioAssetBundleNamesCsv") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("StudioSubRoomDataSaveId") + .HasColumnType("INTEGER"); + + b.Property("StudioUnityAssetId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoomId", "Name"); + + b.HasIndex("RoomId", "OrderIndex") + .IsUnique(); + + b.ToTable("RoomScenes"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoomVisitEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FirstVisitAt") + .HasColumnType("TEXT"); + + b.Property("LastVisitAt") + .HasColumnType("TEXT"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("VisitCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("RoomId", "LastVisitAt"); + + b.HasIndex("RoomId", "PlayerId") + .IsUnique(); + + b.ToTable("RoomVisits"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyaleMatchEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChestsOpened") + .HasColumnType("INTEGER"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("CustomGame") + .HasColumnType("INTEGER"); + + b.Property("HealthPotionsConsumed") + .HasColumnType("INTEGER"); + + b.Property("NumEliminations") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.Property("SecondsAlive") + .HasColumnType("INTEGER"); + + b.Property("SecondsInAir") + .HasColumnType("INTEGER"); + + b.Property("ShieldPotionsConsumed") + .HasColumnType("INTEGER"); + + b.Property("WalkGame") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompletedAt"); + + b.ToTable("RoyaleMatches"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyaleMatchPlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("NumEliminations") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.Property("SecondsAlive") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId"); + + b.HasIndex("MatchId", "PlayerId"); + + b.ToTable("RoyaleMatchPlayers"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RoyalePlayerProgressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CurrentLevelXPThreshold") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("NextLevelAcornReward") + .HasColumnType("INTEGER"); + + b.Property("NextLevelXPThreshold") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("RankIdx") + .HasColumnType("INTEGER"); + + b.Property("RankName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("TotalXP") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("RoyalePlayerProgress"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.ServerSettingsEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("AllAvatarItemsOwned") + .HasColumnType("INTEGER"); + + b.Property("BaseRoomNamesJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CharadesSlotBindingsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DiscoveredGameConfigsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("GlobalFriendsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PlayMenuTagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ProfanityFilterDisabled") + .HasColumnType("INTEGER"); + + b.Property("RecCenterDoorsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("RoomBlobVersionClampDisabled") + .HasColumnType("INTEGER"); + + b.Property("SignupsDisabled") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WeeklyChallengeRewardJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WeeklyChallengesCompletedRequired") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("WeeklyChallengesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.HasKey("Id"); + + b.ToTable("ServerSettings"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.SignupCodeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedByPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Descriptor") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("RedeemedAt") + .HasColumnType("TEXT"); + + b.Property("RedeemedByPlayerId") + .HasColumnType("INTEGER"); + + b.Property("Revoked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SignupCodes"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.StoreItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvailableUntil") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrencyType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsLimitedTime") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Storefront") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("Storefront"); + + b.HasIndex("IsActive", "Category"); + + b.ToTable("StoreItems"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.SubscriptionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("SubscriberPlayerId") + .HasColumnType("INTEGER"); + + b.Property("TargetPlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SubscriberPlayerId"); + + b.HasIndex("TargetPlayerId"); + + b.HasIndex("SubscriberPlayerId", "TargetPlayerId") + .IsUnique(); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.TestCaseEntity", b => + { + b.Property("Pk") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssignedPlayerIdsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AssignedPlayerNamesJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CommentsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("JiraBugUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("JiraUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MinNumAssignedPlayers") + .HasColumnType("INTEGER"); + + b.Property("RoomName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TestPassId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Pk"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TestPassId"); + + b.ToTable("TestCases"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.TestPassEntity", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WasManuallyClosed") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TestPasses"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.UgcPurchasableEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorPlayerId") + .HasColumnType("INTEGER"); + + b.Property("CurrencyType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ImageName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFeatured") + .HasColumnType("INTEGER"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RoomId") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorPlayerId"); + + b.HasIndex("PublicId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("UgcPurchasables"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.AvatarEntity", b => + { + b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Player") + .WithOne("Avatar") + .HasForeignKey("DorkNet.Server.Data.Entities.AvatarEntity", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerSettingEntity", b => + { + b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Player") + .WithMany("Settings") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.RelationshipEntity", b => + { + b.HasOne("DorkNet.Server.Data.Entities.PlayerEntity", "Requester") + .WithMany("Relationships") + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("DorkNet.Server.Data.Entities.PlayerEntity", b => + { + b.Navigation("Avatar"); + + b.Navigation("Relationships"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.cs b/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.cs new file mode 100644 index 0000000..31fb59d --- /dev/null +++ b/DorkNet.Server/Migrations/20260801221530_ClubChatDefaultRepair.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DorkNet.Server.Migrations +{ + /// One-shot data repair for databases that applied + /// ClubPermissionsGalleryAndModeration while it carried the wrong column + /// defaults (ClubChatEnabled false, CommentsJson ""). Those defaults + /// contradicted the entity contract — every pre-existing club had its + /// chat silently disabled and every pre-existing test case held invalid + /// JSON. A migration (rather than a bootstrap statement) because the flip + /// must run exactly once: re-running it on every boot would override any + /// club that later disables chat on purpose. Fresh databases run the + /// corrected AddColumn defaults first, so both UPDATEs no-op there. + public partial class ClubChatDefaultRepair : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"UPDATE ""Clubs"" SET ""ClubChatEnabled"" = TRUE WHERE ""ClubChatEnabled"" = FALSE;"); + migrationBuilder.Sql( + @"UPDATE ""TestCases"" SET ""CommentsJson"" = '[]' WHERE ""CommentsJson"" = '';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Data repair — nothing meaningful to undo. + } + } +} diff --git a/DorkNet.Server/Services/PlayerAccountPurge.cs b/DorkNet.Server/Services/PlayerAccountPurge.cs index ec9ae2f..add8bc2 100644 --- a/DorkNet.Server/Services/PlayerAccountPurge.cs +++ b/DorkNet.Server/Services/PlayerAccountPurge.cs @@ -144,6 +144,19 @@ public static class PlayerAccountPurge .SetProperty(x => x.DeletedAt, now) .SetProperty(x => x.IsPublic, false)); + // Custom avatar items: the purged player's ownership rows go away; + // items they CREATED are reassigned to the system account like + // Inventions, because other players may own and be wearing them — + // deleting the item would break those avatars. + deleted += await db.CustomAvatarItemOwnership + .Where(x => x.PlayerId == playerId) + .ExecuteDeleteAsync(); + reassigned += await db.CustomAvatarItems + .Where(x => x.CreatorPlayerId == playerId) + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.CreatorPlayerId, systemPlayerId) + .SetProperty(x => x.UpdatedAt, now)); + reassigned += await db.SignupCodes .Where(x => x.RedeemedByPlayerId == playerId) .ExecuteUpdateAsync(s => s.SetProperty(x => x.RedeemedByPlayerId, (long?)null)); diff --git a/DorkNet.Server/Startup/DatabaseBootstrap.cs b/DorkNet.Server/Startup/DatabaseBootstrap.cs index f2c3f41..aed7565 100644 --- a/DorkNet.Server/Startup/DatabaseBootstrap.cs +++ b/DorkNet.Server/Startup/DatabaseBootstrap.cs @@ -906,10 +906,24 @@ public static class DatabaseBootstrap @"ALTER TABLE ""Clubs"" ADD COLUMN ""ClubChatEnabled"" INTEGER NOT NULL DEFAULT 1;", @"ALTER TABLE ""ChatMessages"" ADD COLUMN ""ModerationState"" INTEGER NOT NULL DEFAULT 0;", @"ALTER TABLE ""TestCases"" ADD COLUMN ""CommentsJson"" TEXT NOT NULL DEFAULT '[]';", + // Repair rows created while the ClubPermissionsGalleryAndModeration + // migration carried defaultValue: "" — the empty string is not + // valid JSON and made the comment reader throw. Idempotent: '' + // is never a legitimate value for this column. + @"UPDATE ""TestCases"" SET ""CommentsJson"" = '[]' WHERE ""CommentsJson"" = '';", }; foreach (var sql in sqlite) { - try { await db.Database.ExecuteSqlRawAsync(sql); } catch { } + // Only the duplicate-column error is expected (the column already + // exists via Migrate() or a previous boot). Anything else — locked + // database, missing table, malformed schema — must fail startup + // here rather than as a 500 on the first endpoint that needs the + // column. + try { await db.Database.ExecuteSqlRawAsync(sql); } + catch (Exception ex) when ( + ex.Message.Contains("duplicate column name", StringComparison.OrdinalIgnoreCase)) + { + } } var sqliteTables = new[] @@ -940,9 +954,11 @@ public static class DatabaseBootstrap """, @"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubAdditionalImages_ClubId_Slot"" ON ""ClubAdditionalImages"" (""ClubId"", ""Slot"");", }; + // CREATE ... IF NOT EXISTS is already idempotent; a failure here is a + // real problem and must fail startup. foreach (var sql in sqliteTables) { - try { await db.Database.ExecuteSqlRawAsync(sql); } catch { } + await db.Database.ExecuteSqlRawAsync(sql); } } diff --git a/DorkNet.Server/Startup/RequestTracingExtensions.cs b/DorkNet.Server/Startup/RequestTracingExtensions.cs index d3653b7..0ceadb9 100644 --- a/DorkNet.Server/Startup/RequestTracingExtensions.cs +++ b/DorkNet.Server/Startup/RequestTracingExtensions.cs @@ -263,8 +263,13 @@ public static class RequestTracingExtensions private static string[] ResolveSuccessBodyPaths() { var configured = Environment.GetEnvironmentVariable("Trace__SuccessBodyPaths"); + // "/thread" is deliberately absent from the defaults: successful + // thread payloads carry private chat messages, which RedactSecrets + // does not touch, and they would land in application logs and + // PlayerLogEntry rows. Opt in explicitly via Trace__SuccessBodyPaths + // for a short-lived diagnosis instead. if (configured is null) - return ["/player", "/thread", "/clone", "/rooms/", "/matchmake/"]; + return ["/player", "/clone", "/rooms/", "/matchmake/"]; return configured .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)