From 29b7fc4463c81a6d7d258d97e538652836509735 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Fri, 23 Jan 2026 15:34:23 +0100 Subject: [PATCH] Stable to master (#42599) Ban database refactor (#42495) * Ban DB refactor seems to work at a basic level for PostgreSQL * New ban creation API Supports all the new functionality (multiple players/addresses/hwids/roles/rounds per ban). * Make the migration irreversible * Re-implement ban notifications The server ID check is no longer done as admins may want to place bans spanning multiple rounds irrelevant of the source server. * Fix some split query warnings * Implement migration on SQLite * More comments * Remove required from ban reason SS14.Admin changes would like this * More missing AsSplitQuery() calls * Fix missing ban type filter * Fix old CreateServerBan API with permanent time * Fix department and role ban commands with permanent time * Re-add banhits navigation property Dropped this on accident, SS14.Admin needs it. * More ban API fixes. * Don't fetch ban exemption info for role bans Not relevant, reduces query performance * Regenerate migrations * Fix adminnotes command for players that never connected Would blow up handling null player records. Not a new bug introduced by the refactor, but I ran into it. * Great shame... I accidentally committed submodule update... * Update GDPR scripts * Fix sandbox violation * Fix bans with duplicate info causing DB exceptions Most notably happened with role bans, as multiple departments may include the same role. --- .../Administration/UI/BanList/BanListEui.cs | 17 +- .../UI/BanList/Bans/BanListControl.xaml.cs | 2 +- .../UI/BanList/Bans/BanListLine.xaml.cs | 6 +- .../Administration/UI/BanList/IBanListLine.cs | 2 +- .../RoleBans/RoleBanListControl.xaml.cs | 2 +- .../BanList/RoleBans/RoleBanListLine.xaml.cs | 8 +- .../UI/Notes/AdminNotesLine.xaml.cs | 9 +- .../UI/Notes/AdminNotesLinePopup.xaml.cs | 4 +- .../JobRequirementsManager.cs | 4 +- .../Tests/Commands/PardonCommand.cs | 40 +- .../20260120200503_BanRefactor.Designer.cs | 2125 +++++++++++++++++ .../Postgres/20260120200503_BanRefactor.cs | 535 +++++ .../PostgresServerDbContextModelSnapshot.cs | 790 +++--- .../20260120200455_BanRefactor.Designer.cs | 2044 ++++++++++++++++ .../Sqlite/20260120200455_BanRefactor.cs | 498 ++++ .../SqliteServerDbContextModelSnapshot.cs | 720 +++--- Content.Server.Database/Model.Ban.cs | 328 +++ Content.Server.Database/Model.cs | 315 +-- Content.Server.Database/ModelPostgres.cs | 5 +- Content.Server.Database/ModelSqlite.cs | 8 +- .../Administration/BanList/BanListEui.cs | 80 +- Content.Server/Administration/BanPanelEui.cs | 92 +- .../Administration/Commands/BanCommand.cs | 10 +- .../Administration/Commands/BanListCommand.cs | 2 +- .../Commands/DepartmentBanCommand.cs | 15 +- .../Commands/OpenAdminNotesCommand.cs | 3 +- .../Administration/Commands/PardonCommand.cs | 4 +- .../Administration/Commands/RoleBanCommand.cs | 25 +- .../Commands/RoleBanListCommand.cs | 10 +- .../Managers/BanManager.Notification.cs | 18 +- .../Administration/Managers/BanManager.cs | 354 +-- .../Administration/Managers/IBanManager.cs | 353 ++- .../Administration/Notes/AdminNotesEui.cs | 8 +- .../Notes/AdminNotesExtensions.cs | 18 +- .../Administration/Notes/AdminNotesManager.cs | 25 +- .../Notes/IAdminNotesManager.cs | 3 +- .../Administration/PlayerPanelEui.cs | 7 +- .../Administration/Systems/BwoinkSystem.cs | 2 +- .../Connection/ConnectionManager.cs | 4 +- Content.Server/Database/BanDef.cs | 128 + Content.Server/Database/BanMatcher.cs | 50 +- Content.Server/Database/DatabaseRecords.cs | 53 +- Content.Server/Database/EFCoreExtensions.cs | 37 + Content.Server/Database/ServerBanDef.cs | 93 - Content.Server/Database/ServerDbBase.cs | 330 +-- Content.Server/Database/ServerDbManager.cs | 154 +- Content.Server/Database/ServerDbPostgres.cs | 379 +-- Content.Server/Database/ServerDbSqlite.cs | 298 +-- Content.Server/Database/ServerRoleBanDef.cs | 65 - Content.Server/Database/ServerRoleUnbanDef.cs | 19 - .../{ServerUnbanDef.cs => UnbanDef.cs} | 4 +- Content.Server/IP/IPAddressExt.cs | 16 +- .../Managers/VoteManager.DefaultVotes.cs | 19 +- Content.Shared.Database/Bans.cs | 33 + .../Administration/BanList/BanListEuiState.cs | 6 +- .../Administration/BanList/SharedBan.cs | 20 + .../Administration/BanList/SharedServerBan.cs | 17 - .../BanList/SharedServerRoleBan.cs | 18 - .../{SharedServerUnban.cs => SharedUnban.cs} | 2 +- .../Administration/Notes/SharedAdminNote.cs | 7 +- Content.Shared/Players/MsgRoleBans.cs | 6 +- .../Locale/en-US/job/role-ban-command.ftl | 1 - Resources/engineCommandPerms.yml | 1 + Tools/dump_user_data.py | 37 +- Tools/erase_user_data.py | 24 +- 65 files changed, 7663 insertions(+), 2649 deletions(-) create mode 100644 Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs create mode 100644 Content.Server.Database/Model.Ban.cs create mode 100644 Content.Server/Database/BanDef.cs create mode 100644 Content.Server/Database/EFCoreExtensions.cs delete mode 100644 Content.Server/Database/ServerBanDef.cs delete mode 100644 Content.Server/Database/ServerRoleBanDef.cs delete mode 100644 Content.Server/Database/ServerRoleUnbanDef.cs rename Content.Server/Database/{ServerUnbanDef.cs => UnbanDef.cs} (72%) create mode 100644 Content.Shared.Database/Bans.cs create mode 100644 Content.Shared/Administration/BanList/SharedBan.cs delete mode 100644 Content.Shared/Administration/BanList/SharedServerBan.cs delete mode 100644 Content.Shared/Administration/BanList/SharedServerRoleBan.cs rename Content.Shared/Administration/BanList/{SharedServerUnban.cs => SharedUnban.cs} (81%) diff --git a/Content.Client/Administration/UI/BanList/BanListEui.cs b/Content.Client/Administration/UI/BanList/BanListEui.cs index 2fca1dee52..00b27cd173 100644 --- a/Content.Client/Administration/UI/BanList/BanListEui.cs +++ b/Content.Client/Administration/UI/BanList/BanListEui.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using Content.Client.Administration.UI.BanList.Bans; using Content.Client.Administration.UI.BanList.RoleBans; using Content.Client.Eui; @@ -73,7 +74,7 @@ public sealed class BanListEui : BaseEui return date.ToString("MM/dd/yyyy h:mm tt"); } - public static void SetData(IBanListLine line, SharedServerBan ban) where T : SharedServerBan + public static void SetData(IBanListLine line, SharedBan ban) where T : SharedBan { line.Reason.Text = ban.Reason; line.BanTime.Text = FormatDate(ban.BanTime); @@ -94,20 +95,20 @@ public sealed class BanListEui : BaseEui line.BanningAdmin.Text = ban.BanningAdminName; } - private void OnLineIdsClicked(IBanListLine line) where T : SharedServerBan + private void OnLineIdsClicked(IBanListLine line) where T : SharedBan { _popup?.Close(); _popup = null; var ban = line.Ban; var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value)); - var ip = ban.Address == null + var ip = ban.Addresses.Length == 0 ? string.Empty - : Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address)); - var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId)); - var guid = ban.UserId == null + : Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address)))); + var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds))); + var guid = ban.UserIds.Length == 0 ? string.Empty - : Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString())); + : Loc.GetString("ban-list-guid", ("guid", string.Join(',', ban.UserIds))); _popup = new BanListIdsPopup(id, ip, hwid, guid); diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs index 431087568a..a79fc4a137 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs @@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control RobustXamlLoader.Load(this); } - public void SetBans(List bans) + public void SetBans(List bans) { for (var i = Bans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs index 0c4e6e60d0..f1320ef7b9 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs @@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton; namespace Content.Client.Administration.UI.BanList.Bans; [GenerateTypedNameReferences] -public sealed partial class BanListLine : BoxContainer, IBanListLine +public sealed partial class BanListLine : BoxContainer, IBanListLine { - public SharedServerBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public BanListLine(SharedServerBan ban) + public BanListLine(SharedBan ban) { RobustXamlLoader.Load(this); diff --git a/Content.Client/Administration/UI/BanList/IBanListLine.cs b/Content.Client/Administration/UI/BanList/IBanListLine.cs index 097bae15df..565e707218 100644 --- a/Content.Client/Administration/UI/BanList/IBanListLine.cs +++ b/Content.Client/Administration/UI/BanList/IBanListLine.cs @@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls; namespace Content.Client.Administration.UI.BanList; -public interface IBanListLine where T : SharedServerBan +public interface IBanListLine where T : SharedBan { T Ban { get; } Label Reason { get; } diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs index 1ea751deb7..f217dec5e6 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs @@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control RobustXamlLoader.Load(this); } - public void SetRoleBans(List bans) + public void SetRoleBans(List bans) { for (var i = RoleBans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs index 4f77d662e1..ca0d214e31 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs @@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton; namespace Content.Client.Administration.UI.BanList.RoleBans; [GenerateTypedNameReferences] -public sealed partial class RoleBanListLine : BoxContainer, IBanListLine +public sealed partial class RoleBanListLine : BoxContainer, IBanListLine { - public SharedServerRoleBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public RoleBanListLine(SharedServerRoleBan ban) + public RoleBanListLine(SharedBan ban) { RobustXamlLoader.Load(this); @@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine?)Note.BannedRoles ?? [new BanRoleDef("what", "You should not be seeing this")]); + + var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} "); return FormatBanMessageCommon(banMessage); } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs index 18a5003158..e82b85acb6 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs @@ -32,9 +32,9 @@ public sealed partial class AdminNotesLinePopup : Popup IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id)); TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType)); SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None)); - RoundIdLabel.Text = note.Round == null + RoundIdLabel.Text = note.Rounds.Length == 0 ? Loc.GetString("admin-notes-round-id-unknown") - : Loc.GetString("admin-notes-round-id", ("id", note.Round)); + : Loc.GetString("admin-notes-round-id", ("id", string.Join(',', note.Rounds))); CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName)); CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName)); diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index d085d9005c..9325507c53 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -25,8 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager [Dependency] private readonly IPrototypeManager _prototypes = default!; private readonly Dictionary _roles = new(); - private readonly List _jobBans = new(); - private readonly List _antagBans = new(); + private readonly List> _jobBans = new(); + private readonly List> _antagBans = new(); private readonly List _jobWhitelists = new(); private ISawmill _sawmill = default!; diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs index 9e57cd4b0e..5f77af1b10 100644 --- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs +++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs @@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands // No bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty); }); // Try to pardon a ban that does not exist @@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands // Still no bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty); }); var banReason = "test"; @@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands // Should have one ban on record now Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); await pair.RunTicksSync(5); @@ -70,17 +70,17 @@ namespace Content.IntegrationTests.Tests.Commands await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2")); // The existing ban is unaffected - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null); - var ban = await sDatabase.GetServerBanAsync(1); + var ban = await sDatabase.GetBanAsync(1); Assert.Multiple(async () => { Assert.That(ban, Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); // Check that it matches Assert.That(ban.Id, Is.EqualTo(1)); - Assert.That(ban.UserId, Is.EqualTo(clientId)); + Assert.That(ban.UserIds, Is.EquivalentTo([clientId])); Assert.That(ban.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError)); Assert.That(ban.ExpirationTime, Is.Not.Null); Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); @@ -95,20 +95,20 @@ namespace Content.IntegrationTests.Tests.Commands await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1")); // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban - var pardonedBan = await sDatabase.GetServerBanAsync(1); + var pardonedBan = await sDatabase.GetBanAsync(1); Assert.Multiple(async () => { // Check that it matches Assert.That(pardonedBan, Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); Assert.That(pardonedBan.Id, Is.EqualTo(1)); - Assert.That(pardonedBan.UserId, Is.EqualTo(clientId)); + Assert.That(pardonedBan.UserIds, Is.EquivalentTo([clientId])); Assert.That(pardonedBan.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError)); Assert.That(pardonedBan.ExpirationTime, Is.Not.Null); Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); @@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands Assert.Multiple(async () => { // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); // Reconnect client. Slightly faster than dirtying the pair. diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs new file mode 100644 index 0000000000..62dde10b99 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs @@ -0,0 +1,2125 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20260120200503_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("boolean") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("boolean") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ipintel_cache_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("real") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs new file mode 100644 index 0000000000..64a20c2933 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs @@ -0,0 +1,535 @@ +using System; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + type = table.Column(type: "smallint", nullable: false), + playtime_at_note = table.Column(type: "interval", nullable: false), + ban_time = table.Column(type: "timestamp with time zone", nullable: false), + expiration_time = table.Column(type: "timestamp with time zone", nullable: true), + reason = table.Column(type: "text", nullable: false), + severity = table.Column(type: "integer", nullable: false), + banning_admin = table.Column(type: "uuid", nullable: true), + last_edited_by_id = table.Column(type: "uuid", nullable: true), + last_edited_at = table.Column(type: "timestamp with time zone", nullable: true), + exempt_flags = table.Column(type: "integer", nullable: false), + auto_delete = table.Column(type: "boolean", nullable: false), + hidden = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + address = table.Column(type: "inet", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.CheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + hwid = table.Column(type: "bytea", nullable: false), + hwid_type = table.Column(type: "integer", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "uuid", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_type = table.Column(type: "text", nullable: false), + role_id = table.Column(type: "text", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + round_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + unbanning_admin = table.Column(type: "uuid", nullable: true), + unban_time = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.Sql(""" + CREATE INDEX "IX_ban_address_address" + ON ban_address + USING gist + (address inet_ops) + INCLUDE (ban_id); + + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql($""" + -- REMOVE: + -- TRUNCATE ban RESTART IDENTITY CASCADE; + + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, {(int)BanType.Server}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Update ID sequence to be after newly inserted IDs. + SELECT setval('ban_ban_id_seq', (SELECT MAX(ban_id) FROM ban)); + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_merge_map; + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_id_map; + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND date_trunc('second', main.ban_time, 'utc') = date_trunc('second', sub.ban_time, 'utc') + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR date_trunc('minute', main.expiration_time, 'utc') = date_trunc('minute', sub.expiration_time, 'utc') + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT INTO + _role_ban_import_id_map + SELECT + DISTINCT ON (merge_id) + nextval('ban_ban_id_seq'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, {(int)BanType.Role}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + split_part(role_id, ':', 1), split_part(role_id, ':', 2), im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + + migrationBuilder.Sql($""" + CREATE OR REPLACE FUNCTION send_server_ban_notification() + RETURNS trigger AS $$ + BEGIN + PERFORM pg_notify( + 'ban_notification', + json_build_object('ban_id', NEW.ban_id)::text + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER notify_on_server_ban_insert + AFTER INSERT ON ban + FOR EACH ROW + WHEN (NEW.type = {(int)BanType.Server}) + EXECUTE FUNCTION send_server_ban_notification(); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 9ab525942c..1c85aac946 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -519,6 +519,221 @@ namespace Content.Server.Database.Migrations.Postgres b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -1069,95 +1284,6 @@ namespace Content.Server.Database.Migrations.Postgres b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("boolean") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("uuid") - .HasColumnName("banning_admin"); - - b.Property("ExemptFlags") - .HasColumnType("integer") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("RoundId") - .HasColumnType("integer") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("integer") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); - - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { b.Property("UserId") @@ -1196,161 +1322,15 @@ namespace Content.Server.Database.Migrations.Postgres .HasColumnName("connection_id"); b.HasKey("Id") - .HasName("PK_server_ban_hit"); - - b.HasIndex("BanId") - .HasDatabaseName("IX_server_ban_hit_ban_id"); - - b.HasIndex("ConnectionId") - .HasDatabaseName("IX_server_ban_hit_connection_id"); - - b.ToTable("server_ban_hit", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("uuid") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("integer") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("integer") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_role_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_role_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_role_ban_round_id"); - - b.ToTable("server_role_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); - - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("role_unban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BanId") - .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("unban_time"); - - b.Property("UnbanningAdmin") - .HasColumnType("uuid") - .HasColumnName("unbanning_admin"); - - b.HasKey("Id") - .HasName("PK_server_role_unban"); - - b.HasIndex("BanId") - .IsUnique(); - - b.ToTable("server_role_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("unban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BanId") - .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("unban_time"); - - b.Property("UnbanningAdmin") - .HasColumnType("uuid") - .HasColumnName("unbanning_admin"); - - b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_server_ban_hit"); b.HasIndex("BanId") - .IsUnique(); + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); - b.ToTable("server_unban", (string)null); + b.ToTable("server_ban_hit", (string)null); }); modelBuilder.Entity("Content.Server.Database.Trait", b => @@ -1380,6 +1360,36 @@ namespace Content.Server.Database.Migrations.Postgres b.ToTable("trait", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => { b.Property("Id") @@ -1664,6 +1674,123 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -1820,70 +1947,14 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -1897,98 +1968,30 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + .HasConstraintName("FK_trait_profile_profile_id"); - b.Navigation("Ban"); + b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .HasForeignKey("Content.Server.Database.Unban", "BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); + .HasConstraintName("FK_unban_ban_ban_id"); b.Navigation("Ban"); }); - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.HasOne("Content.Server.Database.Profile", "Profile") - .WithMany("Traits") - .HasForeignKey("ProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_trait_profile_profile_id"); - - b.Navigation("Profile"); - }); - modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -2023,6 +2026,23 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("Flags"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Navigation("BanHits"); @@ -2052,10 +2072,6 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2104,18 +2120,6 @@ namespace Content.Server.Database.Migrations.Postgres b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs new file mode 100644 index 0000000000..804e3aae27 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs @@ -0,0 +1,2044 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20260120200455_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("INTEGER") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("INTEGER") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ipintel_cache_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("REAL") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection("ConstructionFavorites") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs new file mode 100644 index 0000000000..f813d10945 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs @@ -0,0 +1,498 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + type = table.Column(type: "INTEGER", nullable: false), + playtime_at_note = table.Column(type: "TEXT", nullable: false), + ban_time = table.Column(type: "TEXT", nullable: false), + expiration_time = table.Column(type: "TEXT", nullable: true), + reason = table.Column(type: "TEXT", nullable: false), + severity = table.Column(type: "INTEGER", nullable: false), + banning_admin = table.Column(type: "TEXT", nullable: true), + last_edited_by_id = table.Column(type: "TEXT", nullable: true), + last_edited_at = table.Column(type: "TEXT", nullable: true), + exempt_flags = table.Column(type: "INTEGER", nullable: false), + auto_delete = table.Column(type: "INTEGER", nullable: false), + hidden = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + address = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + hwid = table.Column(type: "BLOB", nullable: false), + hwid_type = table.Column(type: "INTEGER", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + user_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + role_type = table.Column(type: "TEXT", nullable: false), + role_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + round_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + unbanning_admin = table.Column(type: "TEXT", nullable: true), + unban_time = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql(""" + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, 0, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE); + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE); + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND main.ban_time = sub.ban_time + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR main.expiration_time = sub.expiration_time + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT OR IGNORE INTO + _role_ban_import_id_map + SELECT + merge_id + (SELECT seq FROM sqlite_sequence WHERE name = 'ban'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, 1, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + substr(role_id, 1, instr(role_id, ':')-1), + substr(role_id, instr(role_id, ':')+1), + im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 2d2df5e595..b7ae8c5d1f 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -489,6 +489,207 @@ namespace Content.Server.Database.Migrations.Sqlite b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -1010,91 +1211,6 @@ namespace Content.Server.Database.Migrations.Sqlite b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("INTEGER") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExemptFlags") - .HasColumnType("INTEGER") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { b.Property("UserId") @@ -1142,117 +1258,32 @@ namespace Content.Server.Database.Migrations.Sqlite b.ToTable("server_ban_hit", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); - - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); - - b.Property("BanTime") - .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_role_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_role_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_role_ban_round_id"); - - b.ToTable("server_role_ban", null, t => - { - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") - .HasColumnName("role_unban_id"); + .HasColumnName("trait_id"); - b.Property("BanId") + b.Property("ProfileId") .HasColumnType("INTEGER") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("TEXT") - .HasColumnName("unban_time"); + .HasColumnName("profile_id"); - b.Property("UnbanningAdmin") + b.Property("TraitName") + .IsRequired() .HasColumnType("TEXT") - .HasColumnName("unbanning_admin"); + .HasColumnName("trait_name"); b.HasKey("Id") - .HasName("PK_server_role_unban"); + .HasName("PK_trait"); - b.HasIndex("BanId") + b.HasIndex("ProfileId", "TraitName") .IsUnique(); - b.ToTable("server_role_unban", (string)null); + b.ToTable("trait", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1272,37 +1303,12 @@ namespace Content.Server.Database.Migrations.Sqlite .HasColumnName("unbanning_admin"); b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_unban"); b.HasIndex("BanId") .IsUnique(); - b.ToTable("server_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("trait_id"); - - b.Property("ProfileId") - .HasColumnType("INTEGER") - .HasColumnName("profile_id"); - - b.Property("TraitName") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("trait_name"); - - b.HasKey("Id") - .HasName("PK_trait"); - - b.HasIndex("ProfileId", "TraitName") - .IsUnique(); - - b.ToTable("trait", (string)null); + b.ToTable("unban", (string)null); }); modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => @@ -1587,6 +1593,123 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -1743,70 +1866,14 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -1820,98 +1887,30 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + .HasConstraintName("FK_trait_profile_profile_id"); - b.Navigation("Ban"); + b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .HasForeignKey("Content.Server.Database.Unban", "BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); + .HasConstraintName("FK_unban_ban_ban_id"); b.Navigation("Ban"); }); - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.HasOne("Content.Server.Database.Profile", "Profile") - .WithMany("Traits") - .HasForeignKey("ProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_trait_profile_profile_id"); - - b.Navigation("Profile"); - }); - modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -1946,6 +1945,23 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("Flags"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Navigation("BanHits"); @@ -1975,10 +1991,6 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2027,18 +2039,6 @@ namespace Content.Server.Database.Migrations.Sqlite b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Model.Ban.cs b/Content.Server.Database/Model.Ban.cs new file mode 100644 index 0000000000..7d1ee2ab1a --- /dev/null +++ b/Content.Server.Database/Model.Ban.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore; +using NpgsqlTypes; + +// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength + +namespace Content.Server.Database; + +// +// Contains model definitions primarily related to bans. +// + +internal static class ModelBan +{ + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(b => b.CreatedBy) + .WithMany(pl => pl.AdminServerBansCreated) + .HasForeignKey(b => b.BanningAdmin) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasOne(b => b.LastEditedBy) + .WithMany(pl => pl.AdminServerBansLastEdited) + .HasForeignKey(b => b.LastEditedById) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.UserId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .OwnsOne(bp => bp.HWId) + .Property(hwid => hwid.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoundId, bp.BanId }) + .IsUnique(); + + // Following indices have to be made manually by migration, due to limitations in EF Core: + // https://github.com/dotnet/efcore/issues/11336 + // https://github.com/npgsql/efcore.pg/issues/2567 + // modelBuilder.Entity() + // .HasIndex(bp => new { bp.Address, bp.BanId }) + // .IsUnique(); + // modelBuilder.Entity() + // .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid }) + // .IsUnique(); + // (postgres only) + // modelBuilder.Entity() + // .HasIndex(ba => ba.Address) + // .IncludeProperties(ba => ba.BanId) + // .IsUnique() + // .HasMethod("gist") + // .HasOperators("inet_ops"); + + modelBuilder.Entity() + .ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0")); + } +} + +/// +/// Specifies a ban of some kind. +/// +/// +/// +/// Bans come in two types: and , +/// distinguished with . +/// +/// +/// Bans have one or more "matching data", these being , , +/// and entities. If a player's connection info matches any of these, +/// the ban's effects will apply to that player. +/// +/// +/// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually +/// ("unbanned") by an admin, which is stored as an entity existing for this ban. +/// +/// +public sealed class Ban +{ + public int Id { get; set; } + + /// + /// Whether this is a role or server ban. + /// + public required BanType Type { get; set; } + + public TimeSpan PlaytimeAtNote { get; set; } + + /// + /// The time when the ban was applied by an administrator. + /// + public DateTime BanTime { get; set; } + + /// + /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. + /// + public DateTime? ExpirationTime { get; set; } + + /// + /// The administrator-stated reason for applying the ban. + /// + public string Reason { get; set; } = null!; + + /// + /// The severity of the incident + /// + public NoteSeverity Severity { get; set; } + + /// + /// User ID of the admin that initially applied the ban. + /// + [ForeignKey(nameof(CreatedBy))] + public Guid? BanningAdmin { get; set; } + + public Player? CreatedBy { get; set; } + + /// + /// User ID of the admin that last edited the note + /// + [ForeignKey(nameof(LastEditedBy))] + public Guid? LastEditedById { get; set; } + + public Player? LastEditedBy { get; set; } + public DateTime? LastEditedAt { get; set; } + + /// + /// Optional flags that allow adding exemptions to the ban via . + /// + public ServerBanExemptFlags ExemptFlags { get; set; } + + /// + /// Whether this ban should be automatically deleted from the database when it expires. + /// + /// + /// This isn't done automatically by the game, + /// you will need to set up something like a cron job to clear this from your database, + /// using a command like this: + /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" + /// + public bool AutoDelete { get; set; } + + /// + /// Whether to display this ban in the admin remarks (notes) panel + /// + public bool Hidden { get; set; } + + /// + /// If present, an administrator has manually repealed this ban. + /// + public Unban? Unban { get; set; } + + public List? Rounds { get; set; } + public List? Players { get; set; } + public List? Addresses { get; set; } + public List? Hwids { get; set; } + public List? Roles { get; set; } + public List? BanHits { get; set; } +} + +/// +/// Base type for entities that specify ban matching data. +/// +public interface IBanSelector +{ + int BanId { get; } + Ban? Ban { get; } +} + +/// +/// Indicates that a ban was related to a round (e.g. placed on that round). +/// +public sealed class BanRound +{ + public int Id { get; set; } + + /// + /// The ID of the ban to which this round was relevant. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } + + /// + /// The ID of the round to which this ban was relevant to. + /// + [ForeignKey(nameof(Round))] + public int RoundId { get; set; } + + public Round? Round { get; set; } +} + +/// +/// Specifies a player that a matches. +/// +public sealed class BanPlayer : IBanSelector +{ + public int Id { get; set; } + + /// + /// The user ID of the banned player. + /// + public Guid UserId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies an IP address range that a matches. +/// +public sealed class BanAddress : IBanSelector +{ + public int Id { get; set; } + + /// + /// The address range being matched. + /// + public required NpgsqlInet Address { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies a HWID that a matches. +/// +public sealed class BanHwid : IBanSelector +{ + public int Id { get; set; } + + /// + /// The HWID being matched. + /// + public required TypedHwid HWId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// A single role banned among a greater role ban record. +/// +/// +/// s of type should have one or more s +/// to store which roles are actually banned. +/// It is invalid for bans to have entities. +/// +public sealed class BanRole +{ + public int Id { get; set; } + + /// + /// What type of role is being banned. For example Job or Antag. + /// + public required string RoleType { get; set; } + + /// + /// The ID of the role being banned. This is probably something like a prototype. + /// + public required string RoleId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// An explicit repeal of a by an administrator. +/// Having an entry for a ban neutralizes it. +/// +public sealed class Unban +{ + public int Id { get; set; } + + /// + /// The ID of ban that is being repealed. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + /// + /// The ban that is being repealed. + /// + public Ban? Ban { get; set; } + + /// + /// The admin that repealed the ban. + /// + public Guid? UnbanningAdmin { get; set; } + + /// + /// The time the ban was repealed. + /// + public DateTime UnbanTime { get; set; } +} diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index ac5b003a73..f54bba7e44 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -9,7 +9,6 @@ using System.Net; using System.Text.Json; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; -using NpgsqlTypes; namespace Content.Server.Database { @@ -31,13 +30,17 @@ namespace Content.Server.Database public DbSet AdminLogPlayer { get; set; } = null!; public DbSet Whitelist { get; set; } = null!; public DbSet Blacklist { get; set; } = null!; - public DbSet Ban { get; set; } = default!; - public DbSet Unban { get; set; } = default!; + public DbSet Ban { get; set; } = default!; + public DbSet BanRound { get; set; } = default!; + public DbSet BanPlayer { get; set; } = default!; + public DbSet BanAddress { get; set; } = default!; + public DbSet BanHwid { get; set; } = default!; + public DbSet BanRole { get; set; } = default!; + public DbSet Unban { get; set; } = default!; public DbSet BanExemption { get; set; } = default!; public DbSet ConnectionLog { get; set; } = default!; public DbSet ServerBanHit { get; set; } = default!; - public DbSet RoleBan { get; set; } = default!; - public DbSet RoleUnban { get; set; } = default!; + public DbSet PlayTime { get; set; } = default!; public DbSet UploadedResourceLog { get; set; } = default!; public DbSet AdminNotes { get; set; } = null!; @@ -145,43 +148,11 @@ namespace Content.Server.Database modelBuilder.Entity() .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId}); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - // Ban exemption can't have flags 0 since that wouldn't exempt anything. // The row should be removed if setting to 0. modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("FlagsNotZero", "flags != 0")); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - modelBuilder.Entity() .HasIndex(p => p.UserId) .IsUnique(); @@ -296,34 +267,6 @@ namespace Content.Server.Database t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen")); - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerRoleBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerRoleBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - modelBuilder.Entity() .HasOne(w => w.Player) .WithMany(p => p.JobWhitelists) @@ -342,26 +285,6 @@ namespace Content.Server.Database .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() .OwnsOne(p => p.HWId) .Property(p => p.Hwid) @@ -371,6 +294,8 @@ namespace Content.Server.Database .OwnsOne(p => p.HWId) .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); + + ModelBan.OnModelCreating(modelBuilder); } public virtual IQueryable SearchLogs(IQueryable query, string searchText) @@ -591,10 +516,8 @@ namespace Content.Server.Database public List AdminMessagesCreated { get; set; } = null!; public List AdminMessagesLastEdited { get; set; } = null!; public List AdminMessagesDeleted { get; set; } = null!; - public List AdminServerBansCreated { get; set; } = null!; - public List AdminServerBansLastEdited { get; set; } = null!; - public List AdminServerRoleBansCreated { get; set; } = null!; - public List AdminServerRoleBansLastEdited { get; set; } = null!; + public List AdminServerBansCreated { get; set; } = null!; + public List AdminServerBansLastEdited { get; set; } = null!; public List JobWhitelists { get; set; } = null!; } @@ -724,30 +647,6 @@ namespace Content.Server.Database [ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!; } - // Used by SS14.Admin - public interface IBanCommon where TUnban : IUnbanCommon - { - int Id { get; set; } - Guid? PlayerUserId { get; set; } - NpgsqlInet? Address { get; set; } - TypedHwid? HWId { get; set; } - DateTime BanTime { get; set; } - DateTime? ExpirationTime { get; set; } - string Reason { get; set; } - NoteSeverity Severity { get; set; } - Guid? BanningAdmin { get; set; } - TUnban? Unban { get; set; } - } - - // Used by SS14.Admin - public interface IUnbanCommon - { - int Id { get; set; } - int BanId { get; set; } - Guid? UnbanningAdmin { get; set; } - DateTime UnbanTime { get; set; } - } - /// /// Flags for use with . /// @@ -785,138 +684,6 @@ namespace Content.Server.Database // @formatter:on } - /// - /// A ban from playing on the server. - /// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server. - /// - /// - /// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing). - /// - [Table("server_ban"), Index(nameof(PlayerUserId))] - public class ServerBan : IBanCommon - { - public int Id { get; set; } - - [ForeignKey("Round")] - public int? RoundId { get; set; } - public Round? Round { get; set; } - - /// - /// The user ID of the banned player. - /// - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - - /// - /// CIDR IP address range of the ban. The whole range can match the ban. - /// - public NpgsqlInet? Address { get; set; } - - /// - /// Hardware ID of the banned player. - /// - public TypedHwid? HWId { get; set; } - - /// - /// The time when the ban was applied by an administrator. - /// - public DateTime BanTime { get; set; } - - /// - /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. - /// - public DateTime? ExpirationTime { get; set; } - - /// - /// The administrator-stated reason for applying the ban. - /// - public string Reason { get; set; } = null!; - - /// - /// The severity of the incident - /// - public NoteSeverity Severity { get; set; } - - /// - /// User ID of the admin that applied the ban. - /// - [ForeignKey("CreatedBy")] - public Guid? BanningAdmin { get; set; } - - public Player? CreatedBy { get; set; } - - /// - /// User ID of the admin that last edited the note - /// - [ForeignKey("LastEditedBy")] - public Guid? LastEditedById { get; set; } - - public Player? LastEditedBy { get; set; } - - /// - /// When the ban was last edited - /// - public DateTime? LastEditedAt { get; set; } - - /// - /// Optional flags that allow adding exemptions to the ban via . - /// - public ServerBanExemptFlags ExemptFlags { get; set; } - - /// - /// If present, an administrator has manually repealed this ban. - /// - public ServerUnban? Unban { get; set; } - - /// - /// Whether this ban should be automatically deleted from the database when it expires. - /// - /// - /// This isn't done automatically by the game, - /// you will need to set up something like a cron job to clear this from your database, - /// using a command like this: - /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" - /// - public bool AutoDelete { get; set; } - - /// - /// Whether to display this ban in the admin remarks (notes) panel - /// - public bool Hidden { get; set; } - - public List BanHits { get; set; } = null!; - } - - /// - /// An explicit repeal of a by an administrator. - /// Having an entry for a ban neutralizes it. - /// - [Table("server_unban")] - public class ServerUnban : IUnbanCommon - { - [Column("unban_id")] public int Id { get; set; } - - /// - /// The ID of ban that is being repealed. - /// - public int BanId { get; set; } - - /// - /// The ban that is being repealed. - /// - public ServerBan Ban { get; set; } = null!; - - /// - /// The admin that repealed the ban. - /// - public Guid? UnbanningAdmin { get; set; } - - /// - /// The time the ban repealed. - /// - public DateTime UnbanTime { get; set; } - } - /// /// An exemption for a specific user to a certain type of . /// @@ -937,7 +704,7 @@ namespace Content.Server.Database /// /// The ban flags to exempt this player from. - /// If any bit overlaps , the ban is ignored. + /// If any bit overlaps , the ban is ignored. /// public ServerBanExemptFlags Flags { get; set; } } @@ -1000,54 +767,10 @@ namespace Content.Server.Database public int BanId { get; set; } public int ConnectionId { get; set; } - public ServerBan Ban { get; set; } = null!; + public Ban Ban { get; set; } = null!; public ConnectionLog Connection { get; set; } = null!; } - [Table("server_role_ban"), Index(nameof(PlayerUserId))] - public sealed class ServerRoleBan : IBanCommon - { - public int Id { get; set; } - public int? RoundId { get; set; } - public Round? Round { get; set; } - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - public NpgsqlInet? Address { get; set; } - public TypedHwid? HWId { get; set; } - - public DateTime BanTime { get; set; } - - public DateTime? ExpirationTime { get; set; } - - public string Reason { get; set; } = null!; - - public NoteSeverity Severity { get; set; } - [ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; } - public Player? CreatedBy { get; set; } - - [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; } - public Player? LastEditedBy { get; set; } - public DateTime? LastEditedAt { get; set; } - - public ServerRoleUnban? Unban { get; set; } - public bool Hidden { get; set; } - - public string RoleId { get; set; } = null!; - } - - [Table("server_role_unban")] - public sealed class ServerRoleUnban : IUnbanCommon - { - [Column("role_unban_id")] public int Id { get; set; } - - public int BanId { get; set; } - public ServerRoleBan Ban { get; set; } = null!; - - public Guid? UnbanningAdmin { get; set; } - - public DateTime UnbanTime { get; set; } - } - [Table("play_time")] public sealed class PlayTime { @@ -1247,31 +970,31 @@ namespace Content.Server.Database /// /// The reason for the ban. /// - /// + /// public string Reason { get; set; } = ""; /// /// Exemptions granted to the ban. /// - /// + /// public ServerBanExemptFlags ExemptFlags { get; set; } /// /// Severity of the ban /// - /// + /// public NoteSeverity Severity { get; set; } /// /// Ban will be automatically deleted once expired. /// - /// + /// public bool AutoDelete { get; set; } /// /// Ban is not visible to players in the remarks menu. /// - /// + /// public bool Hidden { get; set; } } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index 7499d0b0f5..c3f41b7058 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -39,10 +39,7 @@ namespace Content.Server.Database // ReSharper disable StringLiteralTypo // Enforce that an address cannot be IPv6-mapped IPv4. // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); - - modelBuilder.Entity().ToTable( t => + modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); modelBuilder.Entity().ToTable(t => diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index 33bc2bcd72..a64a2af8d9 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -58,13 +58,7 @@ namespace Content.Server.Database ); modelBuilder - .Entity() - .Property(e => e.Address) - .HasColumnType("TEXT") - .HasConversion(ipMaskConverter); - - modelBuilder - .Entity() + .Entity() .Property(e => e.Address) .HasColumnType("TEXT") .HasConversion(ipMaskConverter); diff --git a/Content.Server/Administration/BanList/BanListEui.cs b/Content.Server/Administration/BanList/BanListEui.cs index 2ca126bf16..549a14f673 100644 --- a/Content.Server/Administration/BanList/BanListEui.cs +++ b/Content.Server/Administration/BanList/BanListEui.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; using Content.Server.Administration.Managers; using Content.Server.Database; using Content.Server.EUI; using Content.Shared.Administration; using Content.Shared.Administration.BanList; +using Content.Shared.Database; using Content.Shared.Eui; using Robust.Shared.Network; @@ -22,8 +25,8 @@ public sealed class BanListEui : BaseEui private Guid BanListPlayer { get; set; } private string BanListPlayerName { get; set; } = string.Empty; - private List Bans { get; } = new(); - private List RoleBans { get; } = new(); + private List Bans { get; } = new(); + private List RoleBans { get; } = new(); public override void Opened() { @@ -54,74 +57,38 @@ public sealed class BanListEui : BaseEui private async Task LoadBans(NetUserId userId) { - foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null)) - { - SharedServerUnban? unban = null; - if (ban.Unban is { } unbanDef) - { - var unbanningAdmin = unbanDef.UnbanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); - } - - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; - - if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) - { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); - } - - Bans.Add(new SharedServerBan( - ban.Id, - ban.UserId, - ip, - hwid, - ban.BanTime.UtcDateTime, - ban.ExpirationTime?.UtcDateTime, - ban.Reason, - ban.BanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, - unban - )); - } + await LoadBansCore(userId, BanType.Server, Bans); + await LoadBansCore(userId, BanType.Role, RoleBans); } - private async Task LoadRoleBans(NetUserId userId) + private async Task LoadBansCore(NetUserId userId, BanType banType, List list) { - foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null)) + foreach (var ban in await _db.GetBansAsync(null, userId, null, null, type: banType)) { - SharedServerUnban? unban = null; + SharedUnban? unban = null; if (ban.Unban is { } unbanDef) { var unbanningAdmin = unbanDef.UnbanningAdmin == null ? null : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); + unban = new SharedUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); } - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; + ImmutableArray<(string, int cidrMask)> ips = [("*Hidden*", 0)]; + ImmutableArray hwids = ["*Hidden*"]; if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); + ips = [..ban.Addresses.Select(a => (a.address.ToString(), a.cidrMask))]; + hwids = [..ban.HWIds.Select(h => h.ToString())]; } - RoleBans.Add(new SharedServerRoleBan( + + list.Add(new SharedBan( ban.Id, - ban.UserId, - ip, - hwid, + ban.Type, + ban.UserIds, + ips, + hwids, ban.BanTime.UtcDateTime, ban.ExpirationTime?.UtcDateTime, ban.Reason, @@ -129,7 +96,7 @@ public sealed class BanListEui : BaseEui ? null : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, unban, - ban.Role + ban.Roles )); } } @@ -144,7 +111,6 @@ public sealed class BanListEui : BaseEui string.Empty; await LoadBans(userId); - await LoadRoleBans(userId); StateDirty(); } diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs index 4a4b721872..fceb2b5750 100644 --- a/Content.Server/Administration/BanPanelEui.cs +++ b/Content.Server/Administration/BanPanelEui.cs @@ -26,8 +26,8 @@ public sealed class BanPanelEui : BaseEui private string PlayerName { get; set; } = string.Empty; private IPAddress? LastAddress { get; set; } private ImmutableTypedHwid? LastHwid { get; set; } - private const int Ipv4_CIDR = 32; - private const int Ipv6_CIDR = 64; + private const int Ipv4_CIDR = CreateBanInfo.DefaultMaskIpv4; + private const int Ipv6_CIDR = CreateBanInfo.DefaultMaskIpv6; public BanPanelEui() { @@ -73,6 +73,15 @@ public sealed class BanPanelEui : BaseEui return; } + var isRoleBan = ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0; + + CreateBanInfo banInfo = isRoleBan ? new CreateRoleBanInfo(ban.Reason) : new CreateServerBanInfo(ban.Reason); + + banInfo.WithBanningAdmin(Player.UserId); + banInfo.WithSeverity(ban.Severity); + if (ban.BanDurationMinutes > 0) + banInfo.WithMinutes(ban.BanDurationMinutes); + (IPAddress, int)? addressRange = null; if (ban.IpAddress is not null) { @@ -113,69 +122,46 @@ public sealed class BanPanelEui : BaseEui targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid; } - if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0) + if (addressRange != null) + banInfo.AddAddressRange(addressRange.Value); + + if (targetUid != null) + banInfo.AddUser(targetUid.Value, ban.Target!); + + banInfo.AddHWId(targetHWid); + + if (isRoleBan) { - var now = DateTimeOffset.UtcNow; - foreach (var role in ban.BannedJobs ?? []) + var roleBanInfo = (CreateRoleBanInfo)banInfo; + foreach (var row in ban.BannedJobs ?? []) { - _banManager.CreateRoleBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - role, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason, - now - ); + roleBanInfo.AddJob(row); } - foreach (var role in ban.BannedAntags ?? []) + foreach (var row in ban.BannedAntags ?? []) { - _banManager.CreateRoleBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - role, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason, - now - ); + roleBanInfo.AddAntag(row); } - Close(); - - return; + _banManager.CreateRoleBan(roleBanInfo); } - - if (ban.Erase && targetUid is not null) + else { - try - { - if (_entities.TrySystem(out AdminSystem? adminSystem)) - adminSystem.Erase(targetUid.Value); - } - catch (Exception e) + if (ban.Erase && targetUid is not null) { - _sawmill.Error($"Error while erasing banned player:\n{e}"); + try + { + if (_entities.TrySystem(out AdminSystem? adminSystem)) + adminSystem.Erase(targetUid.Value); + } + catch (Exception e) + { + _sawmill.Error($"Error while erasing banned player:\n{e}"); + } } - } - _banManager.CreateServerBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason - ); + _banManager.CreateServerBan((CreateServerBanInfo)banInfo); + } Close(); } diff --git a/Content.Server/Administration/Commands/BanCommand.cs b/Content.Server/Administration/Commands/BanCommand.cs index f76cfded81..a6d3b10646 100644 --- a/Content.Server/Administration/Commands/BanCommand.cs +++ b/Content.Server/Administration/Commands/BanCommand.cs @@ -90,7 +90,15 @@ public sealed class BanCommand : LocalizedCommands var targetUid = located.UserId; var targetHWid = located.LastHWId; - _bans.CreateServerBan(targetUid, target, player?.UserId, null, targetHWid, minutes, severity, reason); + var banInfo = new CreateServerBanInfo(reason); + banInfo.WithBanningAdmin(player?.UserId); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.WithSeverity(severity); + + _bans.CreateServerBan(banInfo); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/BanListCommand.cs b/Content.Server/Administration/Commands/BanListCommand.cs index ea68788deb..26a8647248 100644 --- a/Content.Server/Administration/Commands/BanListCommand.cs +++ b/Content.Server/Administration/Commands/BanListCommand.cs @@ -39,7 +39,7 @@ public sealed class BanListCommand : LocalizedCommands if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); + var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); if (bans.Count == 0) { diff --git a/Content.Server/Administration/Commands/DepartmentBanCommand.cs b/Content.Server/Administration/Commands/DepartmentBanCommand.cs index 15f9859ca1..40faeb7404 100644 --- a/Content.Server/Administration/Commands/DepartmentBanCommand.cs +++ b/Content.Server/Administration/Commands/DepartmentBanCommand.cs @@ -96,13 +96,20 @@ public sealed class DepartmentBanCommand : IConsoleCommand var targetUid = located.UserId; var targetHWid = located.LastHWId; - // If you are trying to remove the following variable, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Without it the note list will get needlessly cluttered. - var now = DateTimeOffset.UtcNow; + var banInfo = new CreateRoleBanInfo(reason); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.AddUser(targetUid, located.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(targetHWid); + banInfo.WithSeverity(severity); + foreach (var job in departmentProto.Roles) { - _banManager.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, now); + banInfo.AddJob(job); } + + _banManager.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs index 5577e13437..35a6d1096a 100644 --- a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs +++ b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs @@ -3,6 +3,7 @@ using Content.Server.Administration.Notes; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; +using Robust.Shared.Network; namespace Content.Server.Administration.Commands; @@ -46,7 +47,7 @@ public sealed class OpenAdminNotesCommand : LocalizedCommands return; } - await _adminNotes.OpenEui(player, notedPlayer); + await _adminNotes.OpenEui(player, new NetUserId(notedPlayer)); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/PardonCommand.cs b/Content.Server/Administration/Commands/PardonCommand.cs index 5c4417a966..eb292663e2 100644 --- a/Content.Server/Administration/Commands/PardonCommand.cs +++ b/Content.Server/Administration/Commands/PardonCommand.cs @@ -27,7 +27,7 @@ namespace Content.Server.Administration.Commands return; } - var ban = await _dbManager.GetServerBanAsync(banId); + var ban = await _dbManager.GetBanAsync(banId); if (ban == null) { @@ -50,7 +50,7 @@ namespace Content.Server.Administration.Commands return; } - await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now)); + await _dbManager.AddUnbanAsync(new UnbanDef(banId, player?.UserId, DateTimeOffset.Now)); shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId))); } diff --git a/Content.Server/Administration/Commands/RoleBanCommand.cs b/Content.Server/Administration/Commands/RoleBanCommand.cs index c49af32881..f303f31b86 100644 --- a/Content.Server/Administration/Commands/RoleBanCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanCommand.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.Managers; +using Content.Server.Administration.Managers; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Database; @@ -99,12 +97,29 @@ public sealed class RoleBanCommand : IConsoleCommand var targetUid = located.UserId; var targetHWid = located.LastHWId; + var banInfo = new CreateRoleBanInfo(reason); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.AddUser(targetUid, located.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(targetHWid); + banInfo.WithSeverity(severity); + if (_proto.HasIndex(role)) - _bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); + { + banInfo.AddJob(new ProtoId(role)); + } else if (_proto.HasIndex(role)) - _bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); + { + banInfo.AddAntag(new ProtoId(role)); + } else + { shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role))); + return; + } + + _bans.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/RoleBanListCommand.cs b/Content.Server/Administration/Commands/RoleBanListCommand.cs index 8244ded3b2..4abd406cbc 100644 --- a/Content.Server/Administration/Commands/RoleBanListCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanListCommand.cs @@ -1,10 +1,8 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.BanList; +using Content.Server.Administration.BanList; using Content.Server.EUI; using Content.Server.Database; using Content.Shared.Administration; -using Robust.Server.Player; +using Content.Shared.Database; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; @@ -48,7 +46,7 @@ public sealed class RoleBanListCommand : IConsoleCommand if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned); + var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned, type: BanType.Role); if (bans.Count == 0) { @@ -58,7 +56,7 @@ public sealed class RoleBanListCommand : IConsoleCommand foreach (var ban in bans) { - var msg = $"ID: {ban.Id}: Role: {ban.Role} Reason: {ban.Reason}"; + var msg = $"ID: {ban.Id}: Role(s): {string.Join(",", ban.Roles ?? [])} Reason: {ban.Reason}"; shell.WriteLine(msg); } return; diff --git a/Content.Server/Administration/Managers/BanManager.Notification.cs b/Content.Server/Administration/Managers/BanManager.Notification.cs index ff84887f00..d627dc508f 100644 --- a/Content.Server/Administration/Managers/BanManager.Notification.cs +++ b/Content.Server/Administration/Managers/BanManager.Notification.cs @@ -41,14 +41,8 @@ public sealed partial class BanManager private async void ProcessBanNotification(BanNotificationData data) { - if ((await _entryManager.ServerEntity).Id == data.ServerId) - { - _sawmill.Verbose("Not processing ban notification: came from this server"); - return; - } - _sawmill.Verbose($"Processing ban notification for ban {data.BanId}"); - var ban = await _db.GetServerBanAsync(data.BanId); + var ban = await _db.GetBanAsync(data.BanId); if (ban == null) { _sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?"); @@ -86,15 +80,5 @@ public sealed partial class BanManager /// [JsonRequired, JsonPropertyName("ban_id")] public int BanId { get; init; } - - /// - /// The id of the server the ban was made on. - /// This is used to avoid double work checking the ban on the originating server. - /// - /// - /// This is optional in case the ban was made outside a server (SS14.Admin) - /// - [JsonPropertyName("server_id")] - public int? ServerId { get; init; } } } diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs index 17f796e699..ccf76e3995 100644 --- a/Content.Server/Administration/Managers/BanManager.cs +++ b/Content.Server/Administration/Managers/BanManager.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -21,6 +20,7 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Administration.Managers; @@ -43,10 +43,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit private ISawmill _sawmill = default!; public const string SawmillId = "admin.bans"; - public const string PrefixAntag = "Antag:"; - public const string PrefixJob = "Job:"; + public const string DbTypeAntag = "Antag"; + public const string DbTypeJob = "Job"; - private readonly Dictionary> _cachedRoleBans = new(); + private readonly Dictionary> _cachedRoleBans = new(); // Cached ban exemption flags are used to handle private readonly Dictionary _cachedBanExemptions = new(); @@ -72,9 +72,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit var netChannel = player.Channel; ImmutableArray? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId; var modernHwids = netChannel.UserData.ModernHWIds; - var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false); - - var userRoleBans = new List(); + var roleBans = await _db.GetBansAsync( + netChannel.RemoteEndPoint.Address, + player.UserId, + hwId, + modernHwids, + false, + type: BanType.Role); + + var userRoleBans = new List(); foreach (var ban in roleBans) { userRoleBans.Add(ban); @@ -115,43 +121,37 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit } #region Server Bans - public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason) + public async void CreateServerBan(CreateServerBanInfo banInfo) { - DateTimeOffset? expires = null; - if (minutes > 0) + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Server, null); + + await _db.AddBanAsync(banDef); + + if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules)) { - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + // Reset their last read rules. They probably need a refresher! + foreach (var (userId, _) in banInfo.Users) + { + await _db.SetLastReadRules(userId, null); + } } - _systems.TryGetEntitySystem(out var ticker); - int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; - var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; - - var banDef = new ServerBanDef( - null, - target, - addressRange, - hwid, - DateTimeOffset.Now, - expires, - roundId, - playtime, - reason, - severity, - banningAdmin, - null); - - await _db.AddServerBanAsync(banDef); - if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null) - await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher! - var adminName = banningAdmin == null + var adminName = banInfo.BanningAdmin == null ? Loc.GetString("system-user") - : (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); - var targetName = target is null ? "null" : $"{targetUsername} ({target})"; - var addressRangeString = addressRange != null - ? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}" - : "null"; - var hwidString = hwid?.ToString() ?? "null"; + : (await _db.GetPlayerRecordByUserId(banInfo.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); + + var targetName = banInfo.Users.Count == 0 + ? "null" + : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})")); + + var addressRangeString = banInfo.AddressRanges.Count != 0 + ? "null" + : string.Join(", ", banInfo.AddressRanges.Select(a => $"{a.Address}/{a.Mask}")); + + var hwidString = banInfo.HWIds.Count == 0 + ? "null" + : string.Join(", ", banInfo.HWIds); + var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}"; var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii"; @@ -159,12 +159,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit var logMessage = Loc.GetString( key, ("admin", adminName), - ("severity", severity), + ("severity", banDef.Severity), ("expires", expiresString), ("name", targetName), ("ip", addressRangeString), ("hwid", hwidString), - ("reason", reason)); + ("reason", banInfo.Reason)); _sawmill.Info(logMessage); _chat.SendAdminAlert(logMessage); @@ -172,7 +172,19 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit KickMatchingConnectedPlayers(banDef, "newly placed ban"); } - private void KickMatchingConnectedPlayers(ServerBanDef def, string source) + private NoteSeverity GetSeverityForServerBan(CreateBanInfo banInfo, CVarDef defaultCVar) + { + if (banInfo.Severity != null) + return banInfo.Severity.Value; + + if (Enum.TryParse(_cfg.GetCVar(defaultCVar), true, out NoteSeverity parsedSeverity)) + return parsedSeverity; + + _sawmill.Error($"CVar {defaultCVar.Name} has invalid ban severity!"); + return NoteSeverity.None; + } + + private void KickMatchingConnectedPlayers(BanDef def, string source) { foreach (var player in _playerManager.Sessions) { @@ -184,7 +196,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit } } - private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) + private bool BanMatchesPlayer(ICommonSession player, BanDef ban) { var playerInfo = new BanMatcher.PlayerInfo { @@ -201,7 +213,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit return BanMatcher.BanMatches(ban, playerInfo); } - private void KickForBanDef(ICommonSession player, ServerBanDef def) + private void KickForBanDef(ICommonSession player, BanDef def) { var message = def.FormatBanMessage(_cfg, _localizationManager); player.Channel.Disconnect(message); @@ -211,108 +223,154 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit #region Role Bans - // If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset. - public async void CreateRoleBan( - NetUserId? target, - string? targetUsername, - NetUserId? banningAdmin, - (IPAddress, int)? addressRange, - ImmutableTypedHwid? hwid, - ProtoId role, - uint? minutes, - NoteSeverity severity, - string reason, - DateTimeOffset timeOfBan - ) where T : class, IPrototype + public async void CreateRoleBan(CreateRoleBanInfo banInfo) { - string encodedRole; + ImmutableArray roleDefs = + [ + .. ToBanRoleDef(banInfo.JobPrototypes), + .. ToBanRoleDef(banInfo.AntagPrototypes), + ]; - // TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced - // this check has consciously avoided refactoring Job and Antag prototype. - // Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash. + if (roleDefs.Length == 0) + throw new ArgumentException("Must specify at least one role to ban!"); - //TODO remove this check as part of the above refactor - if (_prototypeManager.HasIndex(role) && _prototypeManager.HasIndex(role)) - { - _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype."); + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Role, roleDefs); - return; - } + await AddRoleBan(banDef); - // Don't trust the input: make sure the job or antag actually exists. - if (_prototypeManager.HasIndex(role)) - encodedRole = PrefixJob + role; - else if (_prototypeManager.HasIndex(role)) - encodedRole = PrefixAntag + role; - else - { - _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype."); + var length = expires == null + ? Loc.GetString("cmd-roleban-inf") + : Loc.GetString("cmd-roleban-until", ("expires", expires)); - return; + var targetName = banInfo.Users.Count == 0 + ? "null" + : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})")); + + _chat.SendAdminAlert(Loc.GetString( + "cmd-roleban-success", + ("target", targetName), + ("role", string.Join(", ", roleDefs)), + ("reason", banInfo.Reason), + ("length", length))); + + foreach (var (userId, _) in banInfo.Users) + { + if (_playerManager.TryGetSessionById(userId, out var session)) + SendRoleBans(session); } + } - DateTimeOffset? expires = null; + private async Task<(BanDef Ban, DateTimeOffset? Expires)> CreateBanDef( + CreateBanInfo banInfo, + BanType type, + ImmutableArray? roleBans) + { + if (banInfo.Users.Count == 0 && banInfo.HWIds.Count == 0 && banInfo.AddressRanges.Count == 0) + throw new ArgumentException("Must specify at least one user, HWID, or address range"); - if (minutes > 0) - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + DateTimeOffset? expires = null; + if (banInfo.Duration is { } duration) + expires = DateTimeOffset.Now + duration; - _systems.TryGetEntitySystem(out GameTicker? ticker); - int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; - var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; + ImmutableArray roundIds; + if (banInfo.RoundIds.Count > 0) + { + roundIds = [..banInfo.RoundIds]; + } + else if (_systems.TryGetEntitySystem(out var ticker) && ticker.RoundId != 0) + { + roundIds = [ticker.RoundId]; + } + else + { + roundIds = []; + } - var banDef = new ServerRoleBanDef( + return (new BanDef( null, - target, - addressRange, - hwid, - timeOfBan, + type, + [..banInfo.Users.Select(u => u.UserId)], + [..banInfo.AddressRanges], + [..banInfo.HWIds], + DateTimeOffset.Now, expires, - roundId, - playtime, - reason, - severity, - banningAdmin, + roundIds, + await GetPlayTime(banInfo), + banInfo.Reason, + GetSeverityForServerBan(banInfo, CCVars.ServerBanDefaultSeverity), + banInfo.BanningAdmin, null, - encodedRole); + roles: roleBans), expires); + } + + private async Task GetPlayTime(CreateBanInfo banInfo) + { + var firstPlayer = banInfo.Users.FirstOrNull()?.UserId; + if (firstPlayer == null) + return TimeSpan.Zero; - if (!await AddRoleBan(banDef)) + return (await _db.GetPlayTimes(firstPlayer.Value)) + .Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall) + ?.TimeSpent ?? TimeSpan.Zero; + } + + private IEnumerable ToBanRoleDef(IEnumerable> protoIds) where T : class, IPrototype + { + return protoIds.Select(protoId => { - _chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role))); + // TODO: I have no idea if this check is necessary. The previous code was a complete mess, + // so out of safety I'm leaving this in. + if (_prototypeManager.HasIndex(protoId) && _prototypeManager.HasIndex(protoId)) + { + throw new InvalidOperationException( + $"Creating role ban for {protoId}: cannot create role ban, role is both JobPrototype and AntagPrototype."); + } - return; - } + // Don't trust the input: make sure the role actually exists. + if (!_prototypeManager.HasIndex(protoId)) + throw new UnknownPrototypeException(protoId, typeof(T)); - var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires)); - _chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length))); + return new BanRoleDef(PrototypeKindToDbType(), protoId); + }); + } + + private static string PrototypeKindToDbType() where T : class, IPrototype + { + if (typeof(T) == typeof(JobPrototype)) + return DbTypeJob; - if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session)) - SendRoleBans(session); + if (typeof(T) == typeof(AntagPrototype)) + return DbTypeAntag; + + throw new ArgumentException($"Unknown prototype kind for role bans: {typeof(T)}"); } - private async Task AddRoleBan(ServerRoleBanDef banDef) + private async Task AddRoleBan(BanDef banDef) { - banDef = await _db.AddServerRoleBanAsync(banDef); + banDef = await _db.AddBanAsync(banDef); - if (banDef.UserId != null - && _playerManager.TryGetSessionById(banDef.UserId, out var player) - && _cachedRoleBans.TryGetValue(player, out var cachedBans)) + foreach (var user in banDef.UserIds) { - cachedBans.Add(banDef); + if (_playerManager.TryGetSessionById(user, out var player) + && _cachedRoleBans.TryGetValue(player, out var cachedBans)) + { + cachedBans.Add(banDef); + } } - - return true; } public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) { - var ban = await _db.GetServerRoleBanAsync(banId); + var ban = await _db.GetBanAsync(banId); if (ban == null) { return $"No ban found with id {banId}"; } + if (ban.Type != BanType.Role) + throw new InvalidOperationException("Ban was not a role ban!"); + if (ban.Unban != null) { var response = new StringBuilder("This ban has already been pardoned"); @@ -326,14 +384,17 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit return response.ToString(); } - await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); + await _db.AddUnbanAsync(new UnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); - if (ban.UserId is { } player - && _playerManager.TryGetSessionById(player, out var session) - && _cachedRoleBans.TryGetValue(session, out var roleBans)) + foreach (var user in ban.UserIds) { - roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); - SendRoleBans(session); + if (_playerManager.TryGetSessionById(user, out var session) + && _cachedRoleBans.TryGetValue(session, out var roleBans)) + { + roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); + SendRoleBans(session); + } + } return $"Pardoned ban with id {banId}"; @@ -341,64 +402,69 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit public HashSet>? GetJobBans(NetUserId playerUserId) { - return GetRoleBans(playerUserId, PrefixJob); + return GetRoleBans(playerUserId); } public HashSet>? GetAntagBans(NetUserId playerUserId) { - return GetRoleBans(playerUserId, PrefixAntag); + return GetRoleBans(playerUserId); } - private HashSet>? GetRoleBans(NetUserId playerUserId, string prefix) where T : class, IPrototype + private HashSet>? GetRoleBans(NetUserId playerUserId) where T : class, IPrototype { if (!_playerManager.TryGetSessionById(playerUserId, out var session)) return null; - return GetRoleBans(session, prefix); + return GetRoleBans(session); } - private HashSet>? GetRoleBans(ICommonSession playerSession, string prefix) where T : class, IPrototype + private HashSet>? GetRoleBans(ICommonSession playerSession) where T : class, IPrototype { if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans)) return null; + var dbType = PrototypeKindToDbType(); + return roleBans - .Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal)) - .Select(ban => new ProtoId(ban.Role[prefix.Length..])) + .SelectMany(ban => ban.Roles!.Value) + .Where(role => role.RoleType == dbType) + .Select(role => new ProtoId(role.RoleId)) .ToHashSet(); } - public HashSet? GetRoleBans(NetUserId playerUserId) + public HashSet? GetRoleBans(NetUserId playerUserId) { if (!_playerManager.TryGetSessionById(playerUserId, out var session)) return null; return _cachedRoleBans.TryGetValue(session, out var roleBans) - ? roleBans.Select(banDef => banDef.Role).ToHashSet() + ? roleBans.SelectMany(banDef => banDef.Roles ?? []).ToHashSet() : null; } public bool IsRoleBanned(ICommonSession player, List> jobs) { - return IsRoleBanned(player, jobs, PrefixJob); + return IsRoleBanned(player, jobs); } public bool IsRoleBanned(ICommonSession player, List> antags) { - return IsRoleBanned(player, antags, PrefixAntag); + return IsRoleBanned(player, antags); } - private bool IsRoleBanned(ICommonSession player, List> roles, string prefix) where T : class, IPrototype + private bool IsRoleBanned(ICommonSession player, List> roles) where T : class, IPrototype { var bans = GetRoleBans(player.UserId); if (bans is null || bans.Count == 0) return false; + var dbType = PrototypeKindToDbType(); + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (var role in roles) { - if (bans.Contains(prefix + role)) + if (bans.Contains(new BanRoleDef(dbType, role))) return true; } @@ -407,34 +473,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit public void SendRoleBans(ICommonSession pSession) { - var jobBans = GetRoleBans(pSession, PrefixJob); - var jobBansList = new List(jobBans?.Count ?? 0); - - if (jobBans is not null) - { - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (var encodedId in jobBans) - { - jobBansList.Add(encodedId.ToString().Replace(PrefixJob, "")); - } - } - - var antagBans = GetRoleBans(pSession, PrefixAntag); - var antagBansList = new List(antagBans?.Count ?? 0); - - if (antagBans is not null) - { - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (var encodedId in antagBans) - { - antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, "")); - } - } - var bans = new MsgRoleBans() { - JobBans = jobBansList, - AntagBans = antagBansList, + JobBans = (GetRoleBans(pSession) ?? []).ToList(), + AntagBans = (GetRoleBans(pSession) ?? []).ToList(), }; _sawmill.Debug($"Sent role bans to {pSession.Name}"); diff --git a/Content.Server/Administration/Managers/IBanManager.cs b/Content.Server/Administration/Managers/IBanManager.cs index 1912ebe9ec..633ae968db 100644 --- a/Content.Server/Administration/Managers/IBanManager.cs +++ b/Content.Server/Administration/Managers/IBanManager.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Content.Shared.Database; using Content.Shared.Roles; @@ -13,6 +14,11 @@ public interface IBanManager public void Initialize(); public void Restart(); + /// + /// Create a server ban in the database, blocking connection for matching players. + /// + void CreateServerBan(CreateServerBanInfo banInfo); + /// /// Bans the specified target, address range and / or HWID. One of them must be non-null /// @@ -23,12 +29,44 @@ public interface IBanManager /// Number of minutes to ban for. 0 and null mean permanent /// Severity of the resulting ban note /// Reason for the ban - public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); + [Obsolete("Use CreateServerBan(CreateBanInfo) instead")] + public void CreateServerBan(NetUserId? target, + string? targetUsername, + NetUserId? banningAdmin, + (IPAddress, int)? addressRange, + ImmutableTypedHwid? hwid, + uint? minutes, + NoteSeverity severity, + string reason) + { + var info = new CreateServerBanInfo(reason); + if (target != null) + { + ArgumentNullException.ThrowIfNull(targetUsername); + info.AddUser(target.Value, targetUsername); + } + + if (addressRange != null) + info.AddAddressRange(addressRange.Value); + + if (hwid != null) + info.AddHWId(hwid); + + if (minutes > 0) + info.WithMinutes(minutes.Value); + + if (banningAdmin != null) + info.WithBanningAdmin(banningAdmin.Value); + + info.WithSeverity(severity); + + CreateServerBan(info); + } /// /// Gets a list of prefixed prototype IDs with the player's role bans. /// - public HashSet? GetRoleBans(NetUserId playerUserId); + public HashSet? GetRoleBans(NetUserId playerUserId); /// /// Checks if the player is currently banned from any of the listed roles. @@ -57,33 +95,12 @@ public interface IBanManager public HashSet>? GetAntagBans(NetUserId playerUserId); /// - /// Creates a job ban for the specified target, username or GUID + /// Creates a role ban, preventing matching players from playing said roles. /// - /// Target user, username or GUID, null for none - /// The username of the target, if known - /// The responsible admin for the ban - /// The range of IPs that are to be banned, if known - /// The HWID to be banned, if known - /// The role ID to be banned from. Either an AntagPrototype or a JobPrototype - /// Number of minutes to ban for. 0 and null mean permanent - /// Severity of the resulting ban note - /// Reason for the ban - /// Time when the ban was applied, used for grouping role bans - public void CreateRoleBan( - NetUserId? target, - string? targetUsername, - NetUserId? banningAdmin, - (IPAddress, int)? addressRange, - ImmutableTypedHwid? hwid, - ProtoId role, - uint? minutes, - NoteSeverity severity, - string reason, - DateTimeOffset timeOfBan - ) where T : class, IPrototype; + public void CreateRoleBan(CreateRoleBanInfo banInfo); /// - /// Pardons a role ban for the specified target, username or GUID + /// Pardons a role ban by its ID. /// /// The id of the role ban to pardon. /// The admin, if any, that pardoned the role ban. @@ -96,3 +113,287 @@ public interface IBanManager /// Player's session public void SendRoleBans(ICommonSession pSession); } + +/// +/// Base info to fill out in created ban records. +/// +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public abstract class CreateBanInfo +{ + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv4 = 32; + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv6 = 64; + + internal readonly HashSet<(NetUserId UserId, string UserName)> Users = []; + internal readonly HashSet<(IPAddress Address, int Mask)> AddressRanges = []; + internal readonly HashSet HWIds = []; + internal readonly HashSet RoundIds = []; + internal TimeSpan? Duration; + internal NoteSeverity? Severity; + internal string Reason; + internal NetUserId? BanningAdmin; + + protected CreateBanInfo(string reason) + { + Reason = reason; + } + + /// + /// Add a user to be matched by the ban. + /// + /// + /// Bans can target multiple users at once. + /// + /// The ID of the user. + /// The name of the user (used for logging purposes). + /// The current object, for easy chaining. + public CreateBanInfo AddUser(NetUserId userId, string username) + { + Users.Add((userId, username)); + return this; + } + + /// + /// Add an IP address to be matched by the ban. + /// + /// + /// Bans can target multiple addresses at once. + /// + /// + /// The IP address to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddress(IPAddress? address) + { + if (address == null) + return this; + + return AddAddressRange( + address, + address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange) + { + return AddAddressRange(addressRange.Address, addressRange.Mask); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange(IPAddress address, int mask) + { + AddressRanges.Add((address, mask)); + return this; + } + + /// + /// Add a hardware IP (HWID) to be matched by the ban. + /// + /// + /// Bans can target multiple HWIDs at once. + /// + /// + /// The HWID to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId) + { + if (hwId != null) + HWIds.Add(hwId); + + return this; + } + + /// + /// Add a relevant round ID to this ban. + /// + /// + /// + /// If not specified, the current round ID is used for the ban. + /// Therefore, the first call to this function will replace the round ID, + /// and further calls will add additional round IDs. + /// + /// + /// Bans can target multiple round IDs at once. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo AddRoundId(int roundId) + { + RoundIds.Add(roundId); + return this; + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(int minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithMinutes((uint)minutes); + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(uint minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithDuration(TimeSpan.FromMinutes(minutes)); + } + + /// + /// Set how long the ban will last. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive amount of time. + /// + public CreateBanInfo WithDuration(TimeSpan duration) + { + if (duration <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero."); + + Duration = duration; + return this; + } + + /// + /// Set the severity of the ban. + /// + /// + /// If no severity is specified, the default is specified through server configuration. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo WithSeverity(NoteSeverity severity) + { + Severity = severity; + return this; + } + + /// + /// Set the reason for the ban. + /// + /// + /// This replaces the value given via the object constructor. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithReason(string reason) + { + Reason = reason; + return this; + } + + /// + /// Specify the admin responsible for placing the ban. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin) + { + BanningAdmin = banningAdmin; + return this; + } +} + +/// +/// Stores info to create server ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateServerBanInfo : CreateBanInfo +{ + /// The reason for the server ban. + public CreateServerBanInfo(string reason) : base(reason) + { + } +} + +/// +/// Stores info to create role ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateRoleBanInfo : CreateBanInfo +{ + internal readonly HashSet> AntagPrototypes = []; + internal readonly HashSet> JobPrototypes = []; + + /// The reason for the role ban. + public CreateRoleBanInfo(string reason) : base(reason) + { + } + + /// + /// Add an antag role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddAntag(ProtoId protoId) + { + AntagPrototypes.Add(protoId); + return this; + } + + /// + /// Add a job role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddJob(ProtoId protoId) + { + JobPrototypes.Add(protoId); + return this; + } +} diff --git a/Content.Server/Administration/Notes/AdminNotesEui.cs b/Content.Server/Administration/Notes/AdminNotesEui.cs index d1297b251d..5ecb9c774d 100644 --- a/Content.Server/Administration/Notes/AdminNotesEui.cs +++ b/Content.Server/Administration/Notes/AdminNotesEui.cs @@ -22,7 +22,7 @@ public sealed class AdminNotesEui : BaseEui IoCManager.InjectDependencies(this); } - private Guid NotedPlayer { get; set; } + private NetUserId NotedPlayer { get; set; } private string NotedPlayerName { get; set; } = string.Empty; private bool HasConnectedBefore { get; set; } private Dictionary<(int, NoteType), SharedAdminNote> Notes { get; set; } = new(); @@ -112,7 +112,7 @@ public sealed class AdminNotesEui : BaseEui } } - public async Task ChangeNotedPlayer(Guid notedPlayer) + public async Task ChangeNotedPlayer(NetUserId notedPlayer) { NotedPlayer = notedPlayer; await LoadFromDb(); @@ -120,7 +120,7 @@ public sealed class AdminNotesEui : BaseEui private void NoteModified(SharedAdminNote note) { - if (note.Player != NotedPlayer) + if (!note.Players.Contains(NotedPlayer)) return; Notes[(note.Id, note.NoteType)] = note; @@ -129,7 +129,7 @@ public sealed class AdminNotesEui : BaseEui private void NoteDeleted(SharedAdminNote note) { - if (note.Player != NotedPlayer) + if (!note.Players.Contains(NotedPlayer)) return; Notes.Remove((note.Id, note.NoteType)); diff --git a/Content.Server/Administration/Notes/AdminNotesExtensions.cs b/Content.Server/Administration/Notes/AdminNotesExtensions.cs index 349c7ff3bd..e2ec62ed61 100644 --- a/Content.Server/Administration/Notes/AdminNotesExtensions.cs +++ b/Content.Server/Administration/Notes/AdminNotesExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Linq; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; @@ -11,7 +13,7 @@ public static class AdminNotesExtensions NoteSeverity? severity = null; var secret = false; NoteType type; - string[]? bannedRoles = null; + ImmutableArray? bannedRoles = null; string? unbannedByName = null; DateTime? unbannedTime = null; bool? seen = null; @@ -30,13 +32,13 @@ public static class AdminNotesExtensions type = NoteType.Message; seen = adminMessage.Seen; break; - case ServerBanNoteRecord ban: + case BanNoteRecord { Type: BanType.Server } ban: type = NoteType.ServerBan; severity = ban.Severity; unbannedTime = ban.UnbanTime; unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user"); break; - case ServerRoleBanNoteRecord roleBan: + case BanNoteRecord { Type: BanType.Role } roleBan: type = NoteType.RoleBan; severity = roleBan.Severity; bannedRoles = roleBan.Roles; @@ -48,14 +50,14 @@ public static class AdminNotesExtensions } // There may be bans without a user, but why would we ever be converting them to shared notes? - if (note.Player is null) - throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); + if (note.Players.Length == 0) + throw new ArgumentNullException(nameof(note), "Player user ID cannot be empty for a note"); return new SharedAdminNote( note.Id, - note.Player!.UserId, - note.Round?.Id, - note.Round?.Server.Name, + [..note.Players.Select(p => p.UserId)], + [..note.Rounds.Select(r => r.Id)], + note.Rounds.SingleOrDefault()?.Server.Name, // TODO: Show all server names? note.PlaytimeAtNote, type, note.Message, diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 412b191171..f10cd4e3f9 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -52,7 +52,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit return _admins.HasAdminFlag(admin, AdminFlags.ViewNotes); } - public async Task OpenEui(ICommonSession admin, Guid notedPlayer) + public async Task OpenEui(ICommonSession admin, NetUserId notedPlayer) { var ui = new AdminNotesEui(); _euis.OpenEui(ui, admin); @@ -144,8 +144,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit var note = new SharedAdminNote( noteId, - (NetUserId) player, - roundId, + [(NetUserId) player], + roundId.HasValue ? [roundId.Value] : [], serverName, playtime, type, @@ -172,8 +172,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit NoteType.Note => (await _db.GetAdminNote(id))?.ToShared(), NoteType.Watchlist => (await _db.GetAdminWatchlist(id))?.ToShared(), NoteType.Message => (await _db.GetAdminMessage(id))?.ToShared(), - NoteType.ServerBan => (await _db.GetServerBanAsNoteAsync(id))?.ToShared(), - NoteType.RoleBan => (await _db.GetServerRoleBanAsNoteAsync(id))?.ToShared(), + NoteType.ServerBan or NoteType.RoleBan => (await _db.GetBanAsNoteAsync(id))?.ToShared(), _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type") }; } @@ -200,11 +199,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit case NoteType.Message: await _db.DeleteAdminMessage(noteId, deletedBy.UserId, deletedAt); break; - case NoteType.ServerBan: - await _db.HideServerBanFromNotes(noteId, deletedBy.UserId, deletedAt); - break; - case NoteType.RoleBan: - await _db.HideServerRoleBanFromNotes(noteId, deletedBy.UserId, deletedAt); + case NoteType.ServerBan or NoteType.RoleBan: + await _db.HideBanFromNotes(noteId, deletedBy.UserId, deletedAt); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); @@ -280,15 +276,10 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit case NoteType.Message: await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime); break; - case NoteType.ServerBan: + case NoteType.ServerBan or NoteType.RoleBan: if (severity is null) throw new ArgumentException("Severity cannot be null for a ban", nameof(severity)); - await _db.EditServerBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); - break; - case NoteType.RoleBan: - if (severity is null) - throw new ArgumentException("Severity cannot be null for a role ban", nameof(severity)); - await _db.EditServerRoleBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); + await _db.EditBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); diff --git a/Content.Server/Administration/Notes/IAdminNotesManager.cs b/Content.Server/Administration/Notes/IAdminNotesManager.cs index f54f8a21bd..4e992ba30b 100644 --- a/Content.Server/Administration/Notes/IAdminNotesManager.cs +++ b/Content.Server/Administration/Notes/IAdminNotesManager.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; +using Robust.Shared.Network; using Robust.Shared.Player; namespace Content.Server.Administration.Notes; @@ -16,7 +17,7 @@ public interface IAdminNotesManager bool CanDelete(ICommonSession admin); bool CanEdit(ICommonSession admin); bool CanView(ICommonSession admin); - Task OpenEui(ICommonSession admin, Guid notedPlayer); + Task OpenEui(ICommonSession admin, NetUserId notedPlayer); Task OpenUserNotesEui(ICommonSession player); Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime); Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy); diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs index 7de62ac743..13a5d42a4e 100644 --- a/Content.Server/Administration/PlayerPanelEui.cs +++ b/Content.Server/Administration/PlayerPanelEui.cs @@ -186,11 +186,8 @@ public sealed class PlayerPanelEui : BaseEui { _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId); // This won't get associated ip or hwid bans but they were not placed on this account anyways - _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count; - // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally - // The only way to distinguish whether a role ban is the same is to compare the ban time. - // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now. - _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count(); + _bans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null)).Count; + _roleBans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null, type: BanType.Role)).Count(); } else { diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 91211716b5..172e27ee80 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems } // Check if the user has been banned - var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null); + var ban = await _dbManager.GetBanAsync(null, e.Session.UserId, null, null); if (ban != null) { var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason)); diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 9e6ba89d91..c3a389621c 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -207,7 +207,7 @@ namespace Content.Server.Connection * TODO: Jesus H Christ what is this utter mess of a function * TODO: Break this apart into is constituent steps. */ - private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( + private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( NetConnectingArgs e) { // Check if banned. @@ -228,7 +228,7 @@ namespace Content.Server.Connection return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null); } - var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); + var bans = await _db.GetBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); if (bans.Count > 0) { var firstBan = bans[0]; diff --git a/Content.Server/Database/BanDef.cs b/Content.Server/Database/BanDef.cs new file mode 100644 index 0000000000..d459b47579 --- /dev/null +++ b/Content.Server/Database/BanDef.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Robust.Shared.Configuration; +using Robust.Shared.Network; + + +namespace Content.Server.Database +{ + public sealed class BanDef + { + public int? Id { get; } + public BanType Type { get; } + public ImmutableArray UserIds { get; } + public ImmutableArray<(IPAddress address, int cidrMask)> Addresses { get; } + public ImmutableArray HWIds { get; } + + public DateTimeOffset BanTime { get; } + public DateTimeOffset? ExpirationTime { get; } + public ImmutableArray RoundIds { get; } + public TimeSpan PlaytimeAtNote { get; } + public string Reason { get; } + public NoteSeverity Severity { get; set; } + public NetUserId? BanningAdmin { get; } + public UnbanDef? Unban { get; } + public ServerBanExemptFlags ExemptFlags { get; } + + public ImmutableArray? Roles { get; } + + public BanDef( + int? id, + BanType type, + ImmutableArray userIds, + ImmutableArray<(IPAddress address, int cidrMask)> addresses, + ImmutableArray hwIds, + DateTimeOffset banTime, + DateTimeOffset? expirationTime, + ImmutableArray roundIds, + TimeSpan playtimeAtNote, + string reason, + NoteSeverity severity, + NetUserId? banningAdmin, + UnbanDef? unban, + ServerBanExemptFlags exemptFlags = default, + ImmutableArray? roles = null) + { + if (userIds.Length == 0 && addresses.Length == 0 && hwIds.Length == 0) + { + throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); + } + + addresses = addresses.Select(address => + { + if (address is { address.IsIPv4MappedToIPv6: true } addr) + { + // Fix IPv6-mapped IPv4 addresses + // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. + address = (addr.address.MapToIPv4(), addr.cidrMask - 96); + } + + return address; + }) + .ToImmutableArray(); + + Id = id; + Type = type; + UserIds = userIds; + Addresses = addresses; + HWIds = hwIds; + BanTime = banTime; + ExpirationTime = expirationTime; + RoundIds = roundIds; + PlaytimeAtNote = playtimeAtNote; + Reason = reason; + Severity = severity; + BanningAdmin = banningAdmin; + Unban = unban; + ExemptFlags = exemptFlags; + + switch (Type) + { + case BanType.Server: + if (roles != null) + throw new ArgumentException("Cannot specify roles for server ban types", nameof(roles)); + break; + + case BanType.Role: + if (roles is not { Length: > 0 }) + throw new ArgumentException("Must specify roles for server ban types", nameof(roles)); + if (exemptFlags != 0) + throw new ArgumentException("Role bans cannot have exempt flags", nameof(exemptFlags)); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + + Roles = roles; + } + + public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) + { + string expires; + if (ExpirationTime is { } expireTime) + { + var duration = expireTime - BanTime; + var utc = expireTime.ToUniversalTime(); + expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f"))); + } + else + { + var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal); + expires = !string.IsNullOrWhiteSpace(appeal) + ? loc.GetString("ban-banned-permanent-appeal", ("link", appeal)) + : loc.GetString("ban-banned-permanent"); + } + + return $""" + {loc.GetString("ban-banned-1")} + {loc.GetString("ban-banned-2", ("reason", Reason))} + {expires} + {loc.GetString("ban-banned-3")} + """; + } + } +} diff --git a/Content.Server/Database/BanMatcher.cs b/Content.Server/Database/BanMatcher.cs index f477ccd822..0302c0dc13 100644 --- a/Content.Server/Database/BanMatcher.cs +++ b/Content.Server/Database/BanMatcher.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Linq; using System.Net; using Content.Server.IP; using Content.Shared.Database; @@ -7,7 +8,7 @@ using Robust.Shared.Network; namespace Content.Server.Database; /// -/// Implements logic to match a against a player query. +/// Implements logic to match a against a player query. /// /// /// @@ -29,7 +30,7 @@ public static class BanMatcher /// The ban information. /// Information about the player to match against. /// True if the ban matches the provided player info. - public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) + public static bool BanMatches(BanDef ban, in PlayerInfo player) { var exemptFlags = player.ExemptFlags; // Any flag to bypass BlacklistedRange bans. @@ -39,39 +40,44 @@ public static class BanMatcher if ((ban.ExemptFlags & exemptFlags) != 0) return false; + var playerAddr = player.Address; if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP) - && player.Address != null - && ban.Address is not null - && player.Address.IsInSubnet(ban.Address.Value) + && playerAddr != null + && ban.Addresses.Any(addr => playerAddr.IsInSubnet(addr)) && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer)) { return true; } - if (player.UserId is { } id && ban.UserId == id.UserId) + if (player.UserId is { } id && ban.UserIds.Contains(id)) { return true; } - switch (ban.HWId?.Type) + foreach (var banHwid in ban.HWIds) { - case HwidType.Legacy: - if (player.HWId is { Length: > 0 } hwIdVar - && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - { - return true; - } - break; - case HwidType.Modern: - if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) - { - foreach (var hwid in modernHwIdVar) + switch (banHwid.Type) + { + case HwidType.Legacy: + if (player.HWId is { Length: > 0 } hwIdVar + && hwIdVar.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) { - if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - return true; + return true; } - } - break; + + break; + case HwidType.Modern: + if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) + { + foreach (var hwid in modernHwIdVar) + { + if (hwid.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) + return true; + } + } + + break; + } } return false; diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs index 30fba3434b..63ab45a726 100644 --- a/Content.Server/Database/DatabaseRecords.cs +++ b/Content.Server/Database/DatabaseRecords.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net; using Content.Shared.Database; using Robust.Shared.Network; @@ -12,9 +13,9 @@ public interface IAdminRemarksRecord { public int Id { get; } - public RoundRecord? Round { get; } + public ImmutableArray Rounds { get; } - public PlayerRecord? Player { get; } + public ImmutableArray Players { get; } public TimeSpan PlaytimeAtNote { get; } public string Message { get; } @@ -31,10 +32,11 @@ public interface IAdminRemarksRecord public bool Deleted { get; } } -public sealed record ServerRoleBanNoteRecord( +public sealed record BanNoteRecord( int Id, - RoundRecord? Round, - PlayerRecord? Player, + BanType Type, + ImmutableArray Rounds, + ImmutableArray Players, TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, @@ -44,25 +46,9 @@ public sealed record ServerRoleBanNoteRecord( DateTimeOffset? LastEditedAt, DateTimeOffset? ExpirationTime, bool Deleted, - string[] Roles, PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; - -public sealed record ServerBanNoteRecord( - int Id, - RoundRecord? Round, - PlayerRecord? Player, - TimeSpan PlaytimeAtNote, - string Message, - NoteSeverity Severity, - PlayerRecord? CreatedBy, - DateTimeOffset CreatedAt, - PlayerRecord? LastEditedBy, - DateTimeOffset? LastEditedAt, - DateTimeOffset? ExpirationTime, - bool Deleted, - PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; + DateTime? UnbanTime, + ImmutableArray Roles) : IAdminRemarksRecord; public sealed record AdminNoteRecord( int Id, @@ -79,7 +65,11 @@ public sealed record AdminNoteRecord( bool Deleted, PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, - bool Secret) : IAdminRemarksRecord; + bool Secret) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminWatchlistRecord( int Id, @@ -94,7 +84,11 @@ public sealed record AdminWatchlistRecord( DateTimeOffset? ExpirationTime, bool Deleted, PlayerRecord? DeletedBy, - DateTimeOffset? DeletedAt) : IAdminRemarksRecord; + DateTimeOffset? DeletedAt) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminMessageRecord( int Id, @@ -111,15 +105,18 @@ public sealed record AdminMessageRecord( PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, bool Seen, - bool Dismissed) : IAdminRemarksRecord; - + bool Dismissed) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record PlayerRecord( NetUserId UserId, DateTimeOffset FirstSeenTime, string LastSeenUserName, DateTimeOffset LastSeenTime, - IPAddress LastSeenAddress, + IPAddress? LastSeenAddress, ImmutableTypedHwid? HWId); public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server); diff --git a/Content.Server/Database/EFCoreExtensions.cs b/Content.Server/Database/EFCoreExtensions.cs new file mode 100644 index 0000000000..58dbc4639b --- /dev/null +++ b/Content.Server/Database/EFCoreExtensions.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace Content.Server.Database; + +internal static class EFCoreExtensions +{ + extension(IQueryable query) where TEntity : class + { + public IQueryable ApplyIncludes( + IEnumerable>> properties) + { + var q = query; + foreach (var property in properties) + { + q = q.Include(property); + } + + return q; + } + + public IQueryable ApplyIncludes( + IEnumerable>> properties, + Expression> getDerived) + where TDerived : class + { + var q = query; + foreach (var property in properties) + { + q = q.Include(getDerived).ThenInclude(property); + } + + return q; + } + } +} diff --git a/Content.Server/Database/ServerBanDef.cs b/Content.Server/Database/ServerBanDef.cs deleted file mode 100644 index a09f9e959c..0000000000 --- a/Content.Server/Database/ServerBanDef.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Net; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Robust.Shared.Configuration; -using Robust.Shared.Network; - - -namespace Content.Server.Database -{ - public sealed class ServerBanDef - { - public int? Id { get; } - public NetUserId? UserId { get; } - public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableTypedHwid? HWId { get; } - - public DateTimeOffset BanTime { get; } - public DateTimeOffset? ExpirationTime { get; } - public int? RoundId { get; } - public TimeSpan PlaytimeAtNote { get; } - public string Reason { get; } - public NoteSeverity Severity { get; set; } - public NetUserId? BanningAdmin { get; } - public ServerUnbanDef? Unban { get; } - public ServerBanExemptFlags ExemptFlags { get; } - - public ServerBanDef(int? id, - NetUserId? userId, - (IPAddress, int)? address, - TypedHwid? hwId, - DateTimeOffset banTime, - DateTimeOffset? expirationTime, - int? roundId, - TimeSpan playtimeAtNote, - string reason, - NoteSeverity severity, - NetUserId? banningAdmin, - ServerUnbanDef? unban, - ServerBanExemptFlags exemptFlags = default) - { - if (userId == null && address == null && hwId == null) - { - throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); - } - - if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6) - { - // Fix IPv6-mapped IPv4 addresses - // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - address = (addr.Item1.MapToIPv4(), addr.Item2 - 96); - } - - Id = id; - UserId = userId; - Address = address; - HWId = hwId; - BanTime = banTime; - ExpirationTime = expirationTime; - RoundId = roundId; - PlaytimeAtNote = playtimeAtNote; - Reason = reason; - Severity = severity; - BanningAdmin = banningAdmin; - Unban = unban; - ExemptFlags = exemptFlags; - } - - public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) - { - string expires; - if (ExpirationTime is { } expireTime) - { - var duration = expireTime - BanTime; - var utc = expireTime.ToUniversalTime(); - expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f"))); - } - else - { - var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal); - expires = !string.IsNullOrWhiteSpace(appeal) - ? loc.GetString("ban-banned-permanent-appeal", ("link", appeal)) - : loc.GetString("ban-banned-permanent"); - } - - return $""" - {loc.GetString("ban-banned-1")} - {loc.GetString("ban-banned-2", ("reason", Reason))} - {expires} - {loc.GetString("ban-banned-3")} - """; - } - } -} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 00ad726d50..2c5524f502 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -1,13 +1,13 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; using System.Net; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Logs; -using Content.Server.Administration.Managers; using Content.Shared.Administration.Logs; using Content.Shared.Body; using Content.Shared.Construction.Prototypes; @@ -457,7 +457,7 @@ namespace Content.Server.Database /// /// The ban id to look for. /// The ban with the given id or null if none exist. - public abstract Task GetServerBanAsync(int id); + public abstract Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -469,11 +469,12 @@ namespace Content.Server.Database /// The legacy HWId of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - public abstract Task GetServerBanAsync( + public abstract Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type); /// /// Looks up an user's ban history. @@ -486,17 +487,18 @@ namespace Content.Server.Database /// The modern HWIDs of the user. /// Include pardoned and expired bans. /// The user's ban history. - public abstract Task> GetServerBansAsync( + public abstract Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned); + bool includeUnbanned, + BanType type); - public abstract Task AddServerBanAsync(ServerBanDef serverBan); - public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); + public abstract Task AddBanAsync(BanDef ban); + public abstract Task AddUnbanAsync(UnbanDef unban); - public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public async Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -559,61 +561,23 @@ namespace Content.Server.Database return flags ?? ServerBanExemptFlags.None; } - #endregion - - #region Role Bans - /* - * ROLE BANS - */ - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - public abstract Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - public abstract Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned); - - public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); - public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban); - - public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + protected static List>> GetBanDefIncludes(BanType? type = null) { - await using var db = await GetDb(); - var roleBanDetails = await db.DbContext.RoleBan - .Where(b => b.Id == id) - .Select(b => new { b.BanTime, b.PlayerUserId }) - .SingleOrDefaultAsync(); + List>> list = + [ + b => b.Players!, + b => b.Rounds!, + b => b.Hwids!, + b => b.Unban!, + b => b.Addresses!, + ]; - if (roleBanDetails == default) - return; + if (type != BanType.Server) + list.Add(b => b.Roles!); - await db.DbContext.RoleBan - .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId) - .ExecuteUpdateAsync(setters => setters - .SetProperty(b => b.Severity, severity) - .SetProperty(b => b.Reason, reason) - .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null) - .SetProperty(b => b.LastEditedById, editedBy) - .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime) - ); + return list; } + #endregion #region Playtime @@ -734,6 +698,19 @@ namespace Content.Server.Database if (player == null) return null; + return MakePlayerRecord(player.UserId, player); + } + + protected PlayerRecord MakePlayerRecord(Guid userId, Player? player) + { + if (player == null) + { + // We don't have a record for this player in the database. + // This is possible, for example, when banning people that never connected to the server. + // Just return fallback data here, I guess. + return new PlayerRecord(new NetUserId(userId), default, userId.ToString(), default, null, null); + } + return new PlayerRecord( new NetUserId(player.UserId), new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)), @@ -757,7 +734,7 @@ namespace Content.Server.Database ConnectionDenyReason? denied, int serverId); - public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) { await using var db = await GetDb(); @@ -1371,81 +1348,17 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} entity.Dismissed); } - public async Task GetServerBanAsNoteAsync(int id) + public async Task GetBanAsNoteAsync(int id) { await using var db = await GetDb(); - var ban = await db.DbContext.Ban - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + var ban = await BanRecordQuery(db.DbContext) .SingleOrDefaultAsync(b => b.Id == id); if (ban is null) return null; - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - return new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - ban.BanTime, - MakePlayerRecord(ban.LastEditedBy), - ban.LastEditedAt, - ban.ExpirationTime, - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync(p => - p.UserId == ban.Unban.UnbanningAdmin.Value)), - ban.Unban?.UnbanTime); - } - - public async Task GetServerRoleBanAsNoteAsync(int id) - { - await using var db = await GetDb(); - - var ban = await db.DbContext.RoleBan - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .SingleOrDefaultAsync(b => b.Id == id); - - if (ban is null) - return null; - - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - var unbanningAdmin = - ban.Unban is null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin); - - return new ServerRoleBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - ban.BanTime, - MakePlayerRecord(ban.LastEditedBy), - ban.LastEditedAt, - ban.ExpirationTime, - ban.Hidden, - new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) }, - MakePlayerRecord(unbanningAdmin), - ban.Unban?.UnbanTime); + return await MakeBanNoteRecord(db.DbContext, ban); } public async Task> GetAllAdminRemarks(Guid player) @@ -1466,8 +1379,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} .ToListAsync()).Select(MakeAdminNoteRecord)); notes.AddRange(await GetActiveWatchlistsImpl(db, player)); notes.AddRange(await GetMessagesImpl(db, player)); - notes.AddRange(await GetServerBansAsNotesForUser(db, player)); - notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); + notes.AddRange(await GetBansAsNotesForUser(db, player)); return notes; } public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) @@ -1550,7 +1462,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} await db.DbContext.SaveChangesAsync(); } - public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) + public async Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1563,19 +1475,6 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} await db.DbContext.SaveChangesAsync(); } - public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - await using var db = await GetDb(); - - var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync(); - - roleBan.Hidden = true; - roleBan.LastEditedById = deletedBy; - roleBan.LastEditedAt = deletedAt.UtcDateTime; - - await db.DbContext.SaveChangesAsync(); - } - public async Task> GetVisibleAdminRemarks(Guid player) { await using var db = await GetDb(); @@ -1593,8 +1492,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} .Include(note => note.Player) .ToListAsync()).Select(MakeAdminNoteRecord)); notesCol.AddRange(await GetMessagesImpl(db, player)); - notesCol.AddRange(await GetServerBansAsNotesForUser(db, player)); - notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); + notesCol.AddRange(await GetBansAsNotesForUser(db, player)); return notesCol; } @@ -1657,98 +1555,70 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} await db.DbContext.SaveChangesAsync(); } - // These two are here because they get converted into notes later - protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) + private static IQueryable BanRecordQuery(ServerDbContext dbContext) { - // You can't group queries, as player will not always exist. When it doesn't, the - // whole query returns nothing - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - var bans = await db.DbContext.Ban - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) + return dbContext.Ban .Include(ban => ban.Unban) - .Include(ban => ban.Round) + .Include(ban => ban.Rounds!) + .ThenInclude(r => r.Round) .ThenInclude(r => r!.Server) + .Include(ban => ban.Addresses) + .Include(ban => ban.Players) + .Include(ban => ban.Roles) + .Include(ban => ban.Hwids) .Include(ban => ban.CreatedBy) .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .ToArrayAsync(); - - var banNotes = new List(); - foreach (var ban in bans) - { - var banNote = new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - NormalizeDatabaseTime(ban.BanTime), - MakePlayerRecord(ban.LastEditedBy), - NormalizeDatabaseTime(ban.LastEditedAt), - NormalizeDatabaseTime(ban.ExpirationTime), - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync( - p => p.UserId == ban.Unban.UnbanningAdmin.Value)), - NormalizeDatabaseTime(ban.Unban?.UnbanTime)); + .Include(ban => ban.Unban); + } - banNotes.Add(banNote); - } + private async Task MakeBanNoteRecord(ServerDbContext dbContext, Ban ban) + { + var playerRecords = await AsyncSelect(ban.Players, + async bp => MakePlayerRecord(bp.UserId, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == bp.UserId))); - return banNotes; + return new BanNoteRecord( + ban.Id, + ban.Type, + [..ban.Rounds!.Select(br => MakeRoundRecord(br.Round!))], + [..playerRecords], + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy!), + NormalizeDatabaseTime(ban.BanTime), + MakePlayerRecord(ban.LastEditedBy!), + NormalizeDatabaseTime(ban.LastEditedAt), + NormalizeDatabaseTime(ban.ExpirationTime), + ban.Hidden, + ban.Unban?.UnbanningAdmin == null + ? null + : MakePlayerRecord( + ban.Unban.UnbanningAdmin.Value, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.Unban.UnbanningAdmin.Value)), + NormalizeDatabaseTime(ban.Unban?.UnbanTime), + [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]); } - protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) + // These two are here because they get converted into notes later + protected async Task> GetBansAsNotesForUser(DbGuard db, Guid user) { - // Server side query - var bansQuery = await db.DbContext.RoleBan - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + // You can't group queries, as player will not always exist. When it doesn't, the + // whole query returns nothing + var bans = await BanRecordQuery(db.DbContext) + .AsSplitQuery() + .Where(ban => ban.Players!.Any(bp => bp.UserId == user) && !ban.Hidden) .ToArrayAsync(); - // Client side query, as EF can't do groups yet - var bansEnumerable = bansQuery - .GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null }) - .Select(banGroup => banGroup) - .ToArray(); - - List bans = new(); - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - foreach (var banGroup in bansEnumerable) + var banNotes = new List(); + foreach (var ban in bans) { - var firstBan = banGroup.First(); - Player? unbanningAdmin = null; - - if (firstBan.Unban?.UnbanningAdmin is not null) - unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value); - - bans.Add(new ServerRoleBanNoteRecord( - firstBan.Id, - MakeRoundRecord(firstBan.Round), - MakePlayerRecord(player), - firstBan.PlaytimeAtNote, - firstBan.Reason, - firstBan.Severity, - MakePlayerRecord(firstBan.CreatedBy), - NormalizeDatabaseTime(firstBan.BanTime), - MakePlayerRecord(firstBan.LastEditedBy), - NormalizeDatabaseTime(firstBan.LastEditedAt), - NormalizeDatabaseTime(firstBan.ExpirationTime), - firstBan.Hidden, - banGroup.Select(ban => ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null)).ToArray(), - MakePlayerRecord(unbanningAdmin), - NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); + var banNote = await MakeBanNoteRecord(db.DbContext, ban); + + banNotes.Add(banNote); } - return bans; + return banNotes; } #endregion @@ -1922,5 +1792,19 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} { } + + private static async Task> AsyncSelect( + IEnumerable? enumerable, + Func> selector) + { + var results = new List(); + + foreach (var item in enumerable ?? []) + { + results.Add(await selector(item)); + } + + return [..results]; + } } } diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 5110227b96..9231ef5d75 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -67,7 +67,7 @@ namespace Content.Server.Database /// /// The ban id to look for. /// The ban with the given id or null if none exist. - Task GetServerBanAsync(int id); + Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -79,11 +79,12 @@ namespace Content.Server.Database /// The legacy HWID of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - Task GetServerBanAsync( + Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type = BanType.Server); /// /// Looks up an user's ban history. @@ -95,17 +96,18 @@ namespace Content.Server.Database /// The modern HWIDs of the user. /// If true, bans that have been expired or pardoned are also included. /// The user's ban history. - Task> GetServerBansAsync( + Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true); + bool includeUnbanned=true, + BanType type = BanType.Server); - Task AddServerBanAsync(ServerBanDef serverBan); - Task AddServerUnbanAsync(ServerUnbanDef serverBan); + Task AddBanAsync(BanDef ban); + Task AddUnbanAsync(UnbanDef ban); - public Task EditServerBan( + public Task EditBan( int id, string reason, NoteSeverity severity, @@ -131,45 +133,6 @@ namespace Content.Server.Database #endregion - #region Role Bans - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true); - - Task AddServerRoleBanAsync(ServerRoleBanDef serverBan); - Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan); - - public Task EditServerRoleBan( - int id, - string reason, - NoteSeverity severity, - DateTimeOffset? expiration, - Guid editedBy, - DateTimeOffset editedAt); - #endregion - #region Playtime /// @@ -209,7 +172,7 @@ namespace Content.Server.Database ConnectionDenyReason? denied, int serverId); - Task AddServerBanHitsAsync(int connection, IEnumerable bans); + Task AddServerBanHitsAsync(int connection, IEnumerable bans); #endregion @@ -301,8 +264,7 @@ namespace Content.Server.Database Task GetAdminNote(int id); Task GetAdminWatchlist(int id); Task GetAdminMessage(int id); - Task GetServerBanAsNoteAsync(int id); - Task GetServerRoleBanAsNoteAsync(int id); + Task GetBanAsNoteAsync(int id); Task> GetAllAdminRemarks(Guid player); Task> GetVisibleAdminNotes(Guid player); Task> GetActiveWatchlists(Guid player); @@ -313,8 +275,7 @@ namespace Content.Server.Database Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); /// /// Mark an admin message as being seen by the target player. @@ -522,49 +483,51 @@ namespace Content.Server.Database return RunDbCommand(() => _db.GetAssignedUserIdAsync(name)); } - public Task GetServerBanAsync(int id) + public Task GetBanAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(id)); + return RunDbCommand(() => _db.GetBanAsync(id)); } - public Task GetServerBanAsync( + public Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds)); + return RunDbCommand(() => _db.GetBanAsync(address, userId, hwId, modernHWIds, type)); } - public Task> GetServerBansAsync( + public Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true) + bool includeUnbanned=true, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); + return RunDbCommand(() => _db.GetBansAsync(address, userId, hwId, modernHWIds, includeUnbanned, type)); } - public Task AddServerBanAsync(ServerBanDef serverBan) + public Task AddBanAsync(BanDef ban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerBanAsync(serverBan)); + return RunDbCommand(() => _db.AddBanAsync(ban)); } - public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) + public Task AddUnbanAsync(UnbanDef unban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); + return RunDbCommand(() => _db.AddUnbanAsync(unban)); } - public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); + return RunDbCommand(() => _db.EditBan(id, reason, severity, expiration, editedBy, editedAt)); } public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags) @@ -579,43 +542,6 @@ namespace Content.Server.Database return RunDbCommand(() => _db.GetBanExemption(userId, cancel)); } - #region Role Ban - public Task GetServerRoleBanAsync(int id) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsync(id)); - } - - public Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); - } - - public Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan)); - } - - public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban)); - } - - public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt)); - } - #endregion - #region Playtime public Task> GetPlayTimes(Guid player, CancellationToken cancel) @@ -667,7 +593,7 @@ namespace Content.Server.Database return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId)); } - public Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public Task AddServerBanHitsAsync(int connection, IEnumerable bans) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans)); @@ -928,16 +854,10 @@ namespace Content.Server.Database return RunDbCommand(() => _db.GetAdminMessage(id)); } - public Task GetServerBanAsNoteAsync(int id) + public Task GetBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); - } - - public Task GetServerRoleBanAsNoteAsync(int id) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id)); + return RunDbCommand(() => _db.GetBanAsNoteAsync(id)); } public Task> GetAllAdminRemarks(Guid player) @@ -999,16 +919,10 @@ namespace Content.Server.Database return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); } - public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); - } - - public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) + public Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt)); + return RunDbCommand(() => _db.HideBanFromNotes(id, deletedBy, deletedAt)); } public Task MarkMessageAsSeen(int id, bool dismissedToo) diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index 31584a8d74..f0c56b7295 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -62,24 +62,26 @@ namespace Content.Server.Database } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var query = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(p => p.Id == id); + .ApplyIncludes(GetBanDefIncludes()) + .Where(p => p.Id == id) + .AsSplitQuery(); var ban = await query.SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -90,7 +92,7 @@ namespace Content.Server.Database var exempt = await GetBanExemptionCore(db, userId); var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); - var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer) + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer, type) .OrderByDescending(b => b.BanTime); var ban = await query.FirstOrDefaultAsync(); @@ -98,11 +100,12 @@ namespace Content.Server.Database return ConvertBan(ban); } - public override async Task> GetServerBansAsync(IPAddress? address, + public override async Task> GetBansAsync(IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -111,12 +114,11 @@ namespace Content.Server.Database await using var db = await GetDbImpl(); - var exempt = await GetBanExemptionCore(db, userId); + var exempt = type == BanType.Role ? null : await GetBanExemptionCore(db, userId); var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId); - var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer); - + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer, type); var queryBans = await query.ToArrayAsync(); - var bans = new List(queryBans.Length); + var bans = new List(queryBans.Length); foreach (var ban in queryBans) { @@ -131,7 +133,8 @@ namespace Content.Server.Database return bans; } - private static IQueryable MakeBanLookupQuery( + // This has to return IDs instead of direct objects because otherwise all the includes are too complicated. + private static IQueryable MakeBanLookupQuery( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, @@ -139,308 +142,113 @@ namespace Content.Server.Database DbGuardImpl db, bool includeUnbanned, ServerBanExemptFlags? exemptFlags, - bool newPlayer) + bool newPlayer, + BanType type) { DebugTools.Assert(!(address == null && userId == null && hwId == null)); - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.Ban); - - if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)) - { - var newQ = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(b => b.Address != null - && EF.Functions.ContainsOrEqual(b.Address.Value, address) - && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer)); - - query = query == null ? newQ : query.Union(newQ); - } - - DebugTools.Assert( - query != null, - "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - if (exemptFlags is { } exempt) - { - if (exempt != ServerBanExemptFlags.None) - exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - - query = query.Where(b => (b.ExemptFlags & exempt) == 0); - } - - return query.Distinct(); - } - - private static IQueryable? MakeBanLookupQualityShared( - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbSet set) - where TBan : class, IBanCommon - where TUnban : class, IUnbanCommon - { - IQueryable? query = null; + var selectorQueries = new List>(); if (userId is { } uid) - { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.PlayerUserId == uid.UserId); - - query = query == null ? newQ : query.Union(newQ); - } + selectorQueries.Add(db.DbContext.BanPlayer.Where(b => b.UserId == uid.UserId)); if (hwId != null && hwId.Value.Length > 0) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid.Where(bh => + bh.HWId!.Type == HwidType.Legacy && bh.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()) + )); } if (modernHWIds != null) { foreach (var modernHwid in modernHWIds) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid + .Where(b => b.HWId!.Type == HwidType.Modern + && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()))); } } - return query; - } - - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - - NetUserId? aUid = null; - if (ban.BanningAdmin is {} aGuid) + if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None) + .HasFlag(ServerBanExemptFlags.IP)) { - aUid = new NetUserId(aGuid); + selectorQueries.Add(db.PgDbContext.BanAddress + .Where(ba => EF.Functions.ContainsOrEqual(ba.Address, address) + && !(ba.Ban!.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && + !newPlayer))); } - var unbanDef = ConvertUnban(ban.Unban); - - return new ServerBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - ban.BanTime, - ban.ExpirationTime, - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unbanDef, - ban.ExemptFlags); - } - - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is {} aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerUnbanDef( - unban.Id, - aUid, - unban.UnbanTime); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - HWId = serverBan.HWId, - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.PgDbContext.SaveChangesAsync(); - } - - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.PgDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var query = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id); - - var ban = await query.SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - - } - - public override async Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) - { - if (address == null && userId == null && hwId == null) - { - throw new ArgumentException("Address, userId, and hwId cannot all be null"); - } - - await using var db = await GetDbImpl(); - - var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned) - .OrderByDescending(b => b.BanTime); + DebugTools.Assert( + selectorQueries.Count > 0, + "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - return await QueryRoleBans(query); - } + var selectorQuery = selectorQueries + .Select(q => q.Select(sel => sel.BanId)) + .Aggregate((selectors, queryable) => selectors.Union(queryable)); - private static async Task> QueryRoleBans(IQueryable query) - { - var queryRoleBans = await query.ToArrayAsync(); - var bans = new List(queryRoleBans.Length); + var banQuery = db.DbContext.Ban.Where(b => selectorQuery.Contains(b.Id)); - foreach (var ban in queryRoleBans) + if (!includeUnbanned) { - var banDef = ConvertRoleBan(ban); - - if (banDef != null) - { - bans.Add(banDef); - } + banQuery = banQuery.Where(p => + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } - return bans; - } - - private static IQueryable MakeRoleBanLookupQuery( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbGuardImpl db, - bool includeUnbanned) - { - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.RoleBan); - - if (address != null) + if (exemptFlags is { } exempt) { - var newQ = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address)); + if (exempt != ServerBanExemptFlags.None) + exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - query = query == null ? newQ : query.Union(newQ); + banQuery = banQuery.Where(b => (b.ExemptFlags & exempt) == 0); } - if (!includeUnbanned) - { - query = query?.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - query = query!.Distinct(); - return query; + return banQuery + .Where(b => b.Type == type) + .ApplyIncludes(GetBanDefIncludes(type)) + .AsSplitQuery(); } [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is {} aGuid) { aUid = new NetUserId(aGuid); } - var unbanDef = ConvertRoleUnban(ban.Unban); + var unbanDef = ConvertUnban(ban.Unban); - return new ServerRoleBanDef( + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) + { + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; + } + + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], ban.BanTime, ban.ExpirationTime, - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, unbanDef, - ban.RoleId); + ban.ExemptFlags, + roles); } - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -453,45 +261,54 @@ namespace Content.Server.Database aUid = new NetUserId(aGuid); } - return new ServerRoleUnbanDef( + return new UnbanDef( unban.Id, aUid, unban.UnbanTime); } - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - var ban = new ServerRoleBan - { - Address = serverRoleBan.Address.ToNpgsqlInet(), - HWId = serverRoleBan.HWId, - Reason = serverRoleBan.Reason, - Severity = serverRoleBan.Severity, - BanningAdmin = serverRoleBan.BanningAdmin?.UserId, - BanTime = serverRoleBan.BanTime.UtcDateTime, - ExpirationTime = serverRoleBan.ExpirationTime?.UtcDateTime, - RoundId = serverRoleBan.RoundId, - PlaytimeAtNote = serverRoleBan.PlaytimeAtNote, - PlayerUserId = serverRoleBan.UserId?.UserId, - RoleId = serverRoleBan.Role, + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole + { + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.PgDbContext.RoleBan.Add(ban); + db.PgDbContext.Ban.Add(banEntity); await db.PgDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.PgDbContext.RoleUnban.Add(new ServerRoleUnban + db.PgDbContext.Unban.Add(new Unban { - BanId = serverRoleUnban.BanId, - UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId, - UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.PgDbContext.SaveChangesAsync(); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 3e69ece7f1..6bb1bea45f 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -70,48 +70,52 @@ namespace Content.Server.Database } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var ban = await db.SqliteDbContext.Ban - .Include(p => p.Unban) + .ApplyIncludes(GetBanDefIncludes()) .Where(p => p.Id == id) + .AsSplitQuery() .SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false, type)).FirstOrDefault(); } - public override async Task> GetServerBansAsync( + public override async Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned, type)).ToList(); } - private async Task> GetServerBanQueryAsync( + private async Task> GetBanQueryAsync( DbGuardImpl db, IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { var exempt = await GetBanExemptionCore(db, userId); @@ -119,7 +123,7 @@ namespace Content.Server.Database // SQLite can't do the net masking stuff we need to match IP address ranges. // So just pull down the whole list into memory. - var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt); + var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt, type); var playerInfo = new BanMatcher.PlayerInfo { @@ -136,12 +140,12 @@ namespace Content.Server.Database .Where(b => BanMatcher.BanMatches(b!, playerInfo))!; } - private static async Task> GetAllBans( - SqliteServerDbContext db, + private static async Task> GetAllBans(SqliteServerDbContext db, bool includeUnbanned, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + BanType type) { - IQueryable query = db.Ban.Include(p => p.Unban); + var query = db.Ban.Where(b => b.Type == type).ApplyIncludes(GetBanDefIncludes(type)); if (!includeUnbanned) { query = query.Where(p => @@ -157,269 +161,99 @@ namespace Content.Server.Database query = query.Where(b => (b.ExemptFlags & exempt) == 0); } - return await query.ToListAsync(); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.SqliteDbContext.SaveChangesAsync(); + return await query.AsSplitQuery().ToListAsync(); } - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.SqliteDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var ban = await db.SqliteDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id) - .SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - } - - public override async Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - // SQLite can't do the net masking stuff we need to match IP address ranges. - // So just pull down the whole list into memory. - var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned); - - return queryBans - .Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds)) - .Select(ConvertRoleBan) - .ToList()!; - } - - private static async Task> GetAllRoleBans( - SqliteServerDbContext db, - bool includeUnbanned) - { - IQueryable query = db.RoleBan.Include(p => p.Unban); - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - return await query.ToListAsync(); - } - - private static bool RoleBanMatches( - ServerRoleBan ban, - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds) - { - if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) - { - return true; - } - - if (userId is { } id && ban.PlayerUserId == id.UserId) - { - return true; - } - - switch (ban.HWId?.Type) - { - case HwidType.Legacy: - if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - break; - - case HwidType.Modern: - if (modernHWIds != null) - { - foreach (var modernHWId in modernHWIds) + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole { - if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - } - } - - break; - } - - return false; - } - - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverBan) - { - await using var db = await GetDbImpl(); - - var ban = new ServerRoleBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - RoleId = serverBan.Role, + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.SqliteDbContext.RoleBan.Add(ban); + db.SqliteDbContext.Ban.Add(banEntity); await db.SqliteDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban + db.SqliteDbContext.Unban.Add(new Unban { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.SqliteDbContext.SaveChangesAsync(); } + #endregion [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is { } aGuid) { aUid = new NetUserId(aGuid); } - var unban = ConvertRoleUnban(ban.Unban); - - return new ServerRoleBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), - ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unban, - ban.RoleId); - } - - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is { } aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerRoleUnbanDef( - unban.Id, - aUid, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(unban.UnbanTime, DateTimeKind.Utc)); - } - #endregion - - [return: NotNullIfNotNull(nameof(ban))] - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } + var unban = ConvertUnban(ban.Unban); - NetUserId? aUid = null; - if (ban.BanningAdmin is { } aGuid) + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) { - aUid = new NetUserId(aGuid); + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; } - var unban = ConvertUnban(ban.Unban); - - return new ServerBanDef( + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, - unban); + unban, + ban.ExemptFlags, + roles); } - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -432,7 +266,7 @@ namespace Content.Server.Database aUid = new NetUserId(aGuid); } - return new ServerUnbanDef( + return new UnbanDef( unban.Id, aUid, // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. diff --git a/Content.Server/Database/ServerRoleBanDef.cs b/Content.Server/Database/ServerRoleBanDef.cs deleted file mode 100644 index dda3a82237..0000000000 --- a/Content.Server/Database/ServerRoleBanDef.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Net; -using Content.Shared.Database; -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleBanDef -{ - public int? Id { get; } - public NetUserId? UserId { get; } - public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableTypedHwid? HWId { get; } - - public DateTimeOffset BanTime { get; } - public DateTimeOffset? ExpirationTime { get; } - public int? RoundId { get; } - public TimeSpan PlaytimeAtNote { get; } - public string Reason { get; } - public NoteSeverity Severity { get; set; } - public NetUserId? BanningAdmin { get; } - public ServerRoleUnbanDef? Unban { get; } - public string Role { get; } - - public ServerRoleBanDef( - int? id, - NetUserId? userId, - (IPAddress, int)? address, - ImmutableTypedHwid? hwId, - DateTimeOffset banTime, - DateTimeOffset? expirationTime, - int? roundId, - TimeSpan playtimeAtNote, - string reason, - NoteSeverity severity, - NetUserId? banningAdmin, - ServerRoleUnbanDef? unban, - string role) - { - if (userId == null && address == null && hwId == null) - { - throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); - } - - if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6) - { - // Fix IPv6-mapped IPv4 addresses - // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - address = (addr.Item1.MapToIPv4(), addr.Item2 - 96); - } - - Id = id; - UserId = userId; - Address = address; - HWId = hwId; - BanTime = banTime; - ExpirationTime = expirationTime; - RoundId = roundId; - PlaytimeAtNote = playtimeAtNote; - Reason = reason; - Severity = severity; - BanningAdmin = banningAdmin; - Unban = unban; - Role = role; - } -} diff --git a/Content.Server/Database/ServerRoleUnbanDef.cs b/Content.Server/Database/ServerRoleUnbanDef.cs deleted file mode 100644 index 3960a86808..0000000000 --- a/Content.Server/Database/ServerRoleUnbanDef.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleUnbanDef -{ - public int BanId { get; } - - public NetUserId? UnbanningAdmin { get; } - - public DateTimeOffset UnbanTime { get; } - - public ServerRoleUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) - { - BanId = banId; - UnbanningAdmin = unbanningAdmin; - UnbanTime = unbanTime; - } -} diff --git a/Content.Server/Database/ServerUnbanDef.cs b/Content.Server/Database/UnbanDef.cs similarity index 72% rename from Content.Server/Database/ServerUnbanDef.cs rename to Content.Server/Database/UnbanDef.cs index 3d39a6b90c..1fa4fc2a6a 100644 --- a/Content.Server/Database/ServerUnbanDef.cs +++ b/Content.Server/Database/UnbanDef.cs @@ -2,7 +2,7 @@ namespace Content.Server.Database { - public sealed class ServerUnbanDef + public sealed class UnbanDef { public int BanId { get; } @@ -10,7 +10,7 @@ namespace Content.Server.Database public DateTimeOffset UnbanTime { get; } - public ServerUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) + public UnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) { BanId = banId; UnbanningAdmin = unbanningAdmin; diff --git a/Content.Server/IP/IPAddressExt.cs b/Content.Server/IP/IPAddressExt.cs index a61477e01b..8c514e96b9 100644 --- a/Content.Server/IP/IPAddressExt.cs +++ b/Content.Server/IP/IPAddressExt.cs @@ -11,22 +11,14 @@ namespace Content.Server.IP { // Npgsql used to map inet types as a tuple like this. // I'm upgrading the dependencies and I don't wanna rewrite a bunch of DB code, so a few helpers it shall be. - [return: NotNullIfNotNull(nameof(tuple))] - public static NpgsqlInet? ToNpgsqlInet(this (IPAddress, int)? tuple) + public static NpgsqlInet ToNpgsqlInet(this (IPAddress, int) tuple) { - if (tuple == null) - return null; - - return new NpgsqlInet(tuple.Value.Item1, (byte) tuple.Value.Item2); + return new NpgsqlInet(tuple.Item1, (byte)tuple.Item2); } - [return: NotNullIfNotNull(nameof(inet))] - public static (IPAddress, int)? ToTuple(this NpgsqlInet? inet) + public static (IPAddress, int) ToTuple(this NpgsqlInet inet) { - if (inet == null) - return null; - - return (inet.Value.Address, inet.Value.Netmask); + return (inet.Address, inet.Netmask); } // Taken from https://stackoverflow.com/a/56461160/4678631 diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 614a54e0f6..a34c90c363 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -371,14 +371,7 @@ namespace Content.Server.Voting.Managers } var targetUid = located.UserId; var targetHWid = located.LastHWId; - (IPAddress, int)? targetIP = null; - - if (located.LastAddress is not null) - { - targetIP = located.LastAddress.AddressFamily is AddressFamily.InterNetwork - ? (located.LastAddress, 32) // People with ipv4 addresses get a /32 address so we ban that - : (located.LastAddress, 64); // This can only be an ipv6 address. People with ipv6 address should get /64 addresses so we ban that. - } + var targetIP = located.LastAddress; if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession)) { @@ -544,7 +537,15 @@ namespace Content.Server.Voting.Managers uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration); - _bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, Loc.GetString("votekick-ban-reason", ("reason", reason))); + var banInfo = new CreateServerBanInfo(Loc.GetString("votekick-ban-reason", ("reason", reason))); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + banInfo.AddAddress(targetIP); + banInfo.WithSeverity(severity); + if (minutes > 0) + banInfo.WithMinutes(minutes); + + _bans.CreateServerBan(banInfo); } } else diff --git a/Content.Shared.Database/Bans.cs b/Content.Shared.Database/Bans.cs new file mode 100644 index 0000000000..9fb100f5db --- /dev/null +++ b/Content.Shared.Database/Bans.cs @@ -0,0 +1,33 @@ +namespace Content.Shared.Database; + +/// +/// Types of bans that can be stored in the database. +/// +public enum BanType : byte +{ + /// + /// A ban from the entire server. If a player matches the ban info, they will be refused connection. + /// + Server, + + /// + /// A ban from playing one or more roles. + /// + Role, +} + +/// +/// A single role for a database role ban. +/// +/// The type of role being banned, e.g. Job. +/// +/// The ID of the role being banned. This is likely a prototype ID based on . +/// +[Serializable] +public record struct BanRoleDef(string RoleType, string RoleId) +{ + public override string ToString() + { + return $"{RoleType}:{RoleId}"; + } +} diff --git a/Content.Shared/Administration/BanList/BanListEuiState.cs b/Content.Shared/Administration/BanList/BanListEuiState.cs index 09faa9706e..c885ff1f70 100644 --- a/Content.Shared/Administration/BanList/BanListEuiState.cs +++ b/Content.Shared/Administration/BanList/BanListEuiState.cs @@ -6,7 +6,7 @@ namespace Content.Shared.Administration.BanList; [Serializable, NetSerializable] public sealed class BanListEuiState : EuiStateBase { - public BanListEuiState(string banListPlayerName, List bans, List roleBans) + public BanListEuiState(string banListPlayerName, List bans, List roleBans) { BanListPlayerName = banListPlayerName; Bans = bans; @@ -14,6 +14,6 @@ public sealed class BanListEuiState : EuiStateBase } public string BanListPlayerName { get; } - public List Bans { get; } - public List RoleBans { get; } + public List Bans { get; } + public List RoleBans { get; } } diff --git a/Content.Shared/Administration/BanList/SharedBan.cs b/Content.Shared/Administration/BanList/SharedBan.cs new file mode 100644 index 0000000000..68ccc02c58 --- /dev/null +++ b/Content.Shared/Administration/BanList/SharedBan.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; +using Content.Shared.Database; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration.BanList; + +[Serializable, NetSerializable] +public record SharedBan( + int? Id, + BanType Type, + ImmutableArray UserIds, + ImmutableArray<(string address, int cidrMask)> Addresses, + ImmutableArray HWIds, + DateTime BanTime, + DateTime? ExpirationTime, + string Reason, + string? BanningAdminName, + SharedUnban? Unban, + ImmutableArray? Roles); diff --git a/Content.Shared/Administration/BanList/SharedServerBan.cs b/Content.Shared/Administration/BanList/SharedServerBan.cs deleted file mode 100644 index a8b9ce0d9a..0000000000 --- a/Content.Shared/Administration/BanList/SharedServerBan.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public record SharedServerBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban -); diff --git a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs b/Content.Shared/Administration/BanList/SharedServerRoleBan.cs deleted file mode 100644 index fca2ea1583..0000000000 --- a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public sealed record SharedServerRoleBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban, - string Role -) : SharedServerBan(Id, UserId, Address, HWId, BanTime, ExpirationTime, Reason, BanningAdminName, Unban); diff --git a/Content.Shared/Administration/BanList/SharedServerUnban.cs b/Content.Shared/Administration/BanList/SharedUnban.cs similarity index 81% rename from Content.Shared/Administration/BanList/SharedServerUnban.cs rename to Content.Shared/Administration/BanList/SharedUnban.cs index f3a57e4159..d60bb9184e 100644 --- a/Content.Shared/Administration/BanList/SharedServerUnban.cs +++ b/Content.Shared/Administration/BanList/SharedUnban.cs @@ -3,7 +3,7 @@ namespace Content.Shared.Administration.BanList; [Serializable, NetSerializable] -public sealed record SharedServerUnban( +public sealed record SharedUnban( string? UnbanningAdmin, DateTime UnbanTime ); diff --git a/Content.Shared/Administration/Notes/SharedAdminNote.cs b/Content.Shared/Administration/Notes/SharedAdminNote.cs index 09d4f3f947..d849d35078 100644 --- a/Content.Shared/Administration/Notes/SharedAdminNote.cs +++ b/Content.Shared/Administration/Notes/SharedAdminNote.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Content.Shared.Database; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -7,8 +8,8 @@ namespace Content.Shared.Administration.Notes; [Serializable, NetSerializable] public sealed record SharedAdminNote( int Id, // Id of note, message, watchlist, ban or role ban. Should be paired with NoteType to uniquely identify a shared admin note. - NetUserId Player, // Notes player - int? Round, // Which round was it added in? + ImmutableArray Players, // Notes player + ImmutableArray Rounds, // Which round was it added in? string? ServerName, // Which server was this added on? TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note NoteType NoteType, // Type of note @@ -20,7 +21,7 @@ public sealed record SharedAdminNote( DateTime CreatedAt, // When was it created? DateTime? LastEditedAt, // When was it last edited? DateTime? ExpiryTime, // Does it expire? - string[]? BannedRoles, // Only valid for role bans. List of banned roles + ImmutableArray? BannedRoles, // Only valid for role bans. List of banned roles DateTime? UnbannedTime, // Only valid for bans. Set if unbanned string? UnbannedByName, // Only valid for bans. Set if unbanned bool? Seen // Only valid for messages, otherwise should be null. Has the user seen this message? diff --git a/Content.Shared/Players/MsgRoleBans.cs b/Content.Shared/Players/MsgRoleBans.cs index bcc28d01d2..e6fc2df3fc 100644 --- a/Content.Shared/Players/MsgRoleBans.cs +++ b/Content.Shared/Players/MsgRoleBans.cs @@ -1,5 +1,7 @@ +using Content.Shared.Roles; using Lidgren.Network; using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Players; @@ -11,8 +13,8 @@ public sealed class MsgRoleBans : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public List JobBans = new(); - public List AntagBans = new(); + public List> JobBans = new(); + public List> AntagBans = new(); public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { diff --git a/Resources/Locale/en-US/job/role-ban-command.ftl b/Resources/Locale/en-US/job/role-ban-command.ftl index 26062c25b7..148fb9a7b0 100644 --- a/Resources/Locale/en-US/job/role-ban-command.ftl +++ b/Resources/Locale/en-US/job/role-ban-command.ftl @@ -44,7 +44,6 @@ cmd-roleban-severity-parse = ${severity} is not a valid severity\n{$help}. cmd-roleban-arg-count = Invalid amount of arguments. cmd-roleban-job-parse = Job {$job} does not exist. cmd-roleban-name-parse = Unable to find a player with that name. -cmd-roleban-existing = {$target} already has a role ban for {$role}. cmd-roleban-success = Role banned {$target} from {$role} with reason {$reason} {$length}. cmd-roleban-inf = permanently diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml index 4520a1da2d..e5fda48f25 100644 --- a/Resources/engineCommandPerms.yml +++ b/Resources/engineCommandPerms.yml @@ -131,6 +131,7 @@ - fuck - replay_recording_start - replay_recording_stop + - transfer_test - Flags: QUERY Commands: diff --git a/Tools/dump_user_data.py b/Tools/dump_user_data.py index 09f9410805..23b2a7523f 100755 --- a/Tools/dump_user_data.py +++ b/Tools/dump_user_data.py @@ -8,7 +8,7 @@ import os import psycopg2 from uuid import UUID -LATEST_DB_MIGRATION = "20250314222016_ConstructionFavorites" +LATEST_DB_MIGRATION = "20260120200503_BanRefactor" def main(): parser = argparse.ArgumentParser() @@ -42,9 +42,8 @@ def main(): dump_player(cur, user_id, arg_output) dump_preference(cur, user_id, arg_output) dump_role_whitelists(cur, user_id, arg_output) - dump_server_ban(cur, user_id, arg_output) + dump_ban(cur, user_id, arg_output) dump_server_ban_exemption(cur, user_id, arg_output) - dump_server_role_ban(cur, user_id, arg_output) dump_uploaded_resource_log(cur, user_id, arg_output) dump_whitelist(cur, user_id, arg_output) @@ -301,7 +300,7 @@ FROM ( f.write(json_data) -def dump_server_ban(cur: "psycopg2.cursor", user_id: str, outdir: str): +def dump_ban(cur: "psycopg2.cursor", user_id: str, outdir: str): print("Dumping server_ban...") cur.execute(""" @@ -311,19 +310,39 @@ FROM ( SELECT *, (SELECT to_jsonb(unban_sq) - 'ban_id' FROM ( - SELECT * FROM server_unban WHERE server_unban.ban_id = server_ban.server_ban_id + SELECT * FROM unban WHERE unban.ban_id = ban.ban_id ) unban_sq) - as unban + as unban, + (SELECT COALESCE(json_agg(to_jsonb(ban_player_subq) - 'ban_id'), '[]') FROM ( + SELECT * FROM ban_player WHERE ban_player.ban_id = ban.ban_id + ) ban_player_subq) + as ban_player, + (SELECT COALESCE(json_agg(to_jsonb(ban_address_subq) - 'ban_id'), '[]') FROM ( + SELECT * FROM ban_address WHERE ban_address.ban_id = ban.ban_id + ) ban_address_subq) + as ban_address, + (SELECT COALESCE(json_agg(to_jsonb(ban_role_subq) - 'ban_id'), '[]') FROM ( + SELECT * FROM ban_role WHERE ban_role.ban_id = ban.ban_id + ) ban_role_subq) + as ban_role, + (SELECT COALESCE(json_agg(to_jsonb(ban_hwid_subq) - 'ban_id'), '[]') FROM ( + SELECT * FROM ban_hwid WHERE ban_hwid.ban_id = ban.ban_id + ) ban_hwid_subq) + as ban_hwid, + (SELECT COALESCE(json_agg(to_jsonb(ban_round_subq) - 'ban_id'), '[]') FROM ( + SELECT * FROM ban_round WHERE ban_round.ban_id = ban.ban_id + ) ban_round_subq) + as ban_round FROM - server_ban + ban WHERE - player_user_id = %s + ban_id IN (SELECT bp.ban_id FROM ban_player bp WHERE bp.user_id = %s) ) as data """, (user_id,)) json_data = cur.fetchall()[0][0] - with open(os.path.join(outdir, "server_ban.json"), "w", encoding="utf-8") as f: + with open(os.path.join(outdir, "ban.json"), "w", encoding="utf-8") as f: f.write(json_data) diff --git a/Tools/erase_user_data.py b/Tools/erase_user_data.py index 509654f91e..dda9042cf7 100644 --- a/Tools/erase_user_data.py +++ b/Tools/erase_user_data.py @@ -12,7 +12,7 @@ import os import psycopg2 from uuid import UUID -LATEST_DB_MIGRATION = "20250314222016_ConstructionFavorites" +LATEST_DB_MIGRATION = "20260120200503_BanRefactor" def main(): parser = argparse.ArgumentParser() @@ -38,9 +38,8 @@ def main(): clear_play_time(cur, user_id) clear_player(cur, user_id) clear_preference(cur, user_id) - clear_server_ban(cur, user_id) + clear_ban(cur, user_id) clear_server_ban_exemption(cur, user_id) - clear_server_role_ban(cur, user_id) clear_uploaded_resource_log(cur, user_id) clear_whitelist(cur, user_id) clear_blacklist(cur, user_id) @@ -144,14 +143,14 @@ WHERE """, (user_id,)) -def clear_server_ban(cur: "psycopg2.cursor", user_id: str): - print("Clearing server_ban...") +def clear_ban(cur: "psycopg2.cursor", user_id: str): + print("Clearing ban...") cur.execute(""" DELETE FROM - server_ban + ban WHERE - player_user_id = %s + ban_id IN (SELECT bp.ban_id FROM ban_player bp WHERE bp.user_id = %s) """, (user_id,)) @@ -166,17 +165,6 @@ WHERE """, (user_id,)) -def clear_server_role_ban(cur: "psycopg2.cursor", user_id: str): - print("Clearing server_role_ban...") - - cur.execute(""" -DELETE FROM - server_role_ban -WHERE - player_user_id = %s -""", (user_id,)) - - def clear_uploaded_resource_log(cur: "psycopg2.cursor", user_id: str): print("Clearing uploaded_resource_log...") -- 2.52.0