feat: add 3D Charades word lists management and profanity filter toggle

- Implemented Charades word lists management in the admin UI, allowing for unlimited named word lists with difficulty settings.
- Added functionality to bind live card slots to specific word lists, with automatic fallback to built-in lists.
- Introduced a profanity filter toggle in server settings, enabling or disabling the server-side profanity filter.
- Updated database schema to support new features, including additional columns for profanity filter status and charades slot bindings.
- Enhanced documentation to reflect new features and their usage.
This commit is contained in:
Alexa
2026-07-02 00:54:04 +03:00
parent fbfabaf26e
commit e002b88d71
19 changed files with 1516 additions and 22 deletions
@@ -103,6 +103,7 @@ public static class DatabaseBootstrap
// IF NOT EXISTS no-ops when EnsureCreated already built them).
await EnsureSignupCodeTablesAsync(db);
await EnsureServerSettingsColumnsAsync(db);
await EnsureCharadesWordListTableAsync(db);
await EnsureInventorySchemaAsync(db);
await EnsureLeaderboardSchemaAsync(db);
await EnsureRoomColumnsAsync(db);
@@ -168,6 +169,13 @@ public static class DatabaseBootstrap
var storeService = scope.ServiceProvider.GetRequiredService<StoreService>();
await storeService.SeedAsync();
// Seed the three built-in 3D Charades word lists (Default / April
// Fools / Icebreakers) and bind each client card-source slot to
// them. Idempotent — no-ops once any list exists so admin edits
// survive restarts.
var charadesService = scope.ServiceProvider.GetRequiredService<CharadesWordListService>();
await charadesService.SeedAsync();
// Tell the /healthz probe migrations are done. Coolify's rolling
// deploy holds traffic at the LB until this flips and /healthz
// returns 200.
@@ -438,6 +446,10 @@ public static class DatabaseBootstrap
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""DiscoveredGameConfigsJson"" text NOT NULL DEFAULT '';");
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""GlobalFriendsEnabled"" boolean NOT NULL DEFAULT false;");
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""ProfanityFilterDisabled"" boolean NOT NULL DEFAULT false;");
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""CharadesSlotBindingsJson"" text NOT NULL DEFAULT '';");
return;
}
@@ -488,6 +500,30 @@ public static class DatabaseBootstrap
// ignore the duplicate-column error; any other schema problem
// will surface when EF reads ServerSettings.
}
try
{
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""ServerSettings"" ADD COLUMN ""ProfanityFilterDisabled"" INTEGER NOT NULL DEFAULT 0;");
}
catch
{
// SQLite has no ADD COLUMN IF NOT EXISTS. If it already exists,
// ignore the duplicate-column error; any other schema problem
// will surface when EF reads ServerSettings.
}
try
{
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""ServerSettings"" ADD COLUMN ""CharadesSlotBindingsJson"" TEXT NOT NULL DEFAULT '';");
}
catch
{
// SQLite has no ADD COLUMN IF NOT EXISTS. If it already exists,
// ignore the duplicate-column error; any other schema problem
// will surface when EF reads ServerSettings.
}
}
private static async Task EnsureInventorySchemaAsync(DorkNetDbContext db)
@@ -625,6 +661,41 @@ public static class DatabaseBootstrap
@"CREATE INDEX IF NOT EXISTS ""IX_LeaderboardStats_RoomId_StatChannel_Value"" ON ""LeaderboardStats"" (""RoomId"", ""StatChannel"", ""Value"");");
}
/// <summary>Idempotently create the CharadesWordLists table on both
/// providers. Post-dates the consolidated Initial migration (same
/// rationale as <see cref="EnsureSignupCodeTablesAsync"/>): Postgres
/// is EnsureCreated-only and the SQLite Initial migration doesn't
/// include it, so we create it here with CREATE TABLE IF NOT EXISTS
/// (a no-op on a fresh DB where EnsureCreated/convention already
/// built it).</summary>
private static async Task EnsureCharadesWordListTableAsync(DorkNetDbContext db)
{
var provider = db.Database.ProviderName ?? string.Empty;
var pg = provider.Contains("Npgsql", StringComparison.OrdinalIgnoreCase);
var sql = pg
? """
CREATE TABLE IF NOT EXISTS "CharadesWordLists" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"Name" text NOT NULL DEFAULT '',
"WordsJson" text NOT NULL DEFAULT '[]',
"IsBuiltIn" boolean NOT NULL DEFAULT false,
"CreatedAt" timestamp with time zone NOT NULL DEFAULT now(),
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT now()
);
"""
: """
CREATE TABLE IF NOT EXISTS "CharadesWordLists" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CharadesWordLists" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NOT NULL DEFAULT '',
"WordsJson" TEXT NOT NULL DEFAULT '[]',
"IsBuiltIn" INTEGER NOT NULL DEFAULT 0,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);
""";
await db.Database.ExecuteSqlRawAsync(sql);
}
private static async Task EnsureMarch2023TablesAsync(DorkNetDbContext db)
{
var provider = db.Database.ProviderName ?? string.Empty;
@@ -217,6 +217,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<PrivateInstanceService>();
services.AddScoped<CommunityBoardService>();
services.AddScoped<ServerSettingsService>();
services.AddScoped<CharadesWordListService>();
// Singletons — these own connectionless state (Redis-backed or
// process-local) and don't need a per-request scope.
services.AddSingleton<NotificationService>();