]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Stable to master (#42599)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Fri, 23 Jan 2026 14:34:23 +0000 (15:34 +0100)
committerGitHub <noreply@github.com>
Fri, 23 Jan 2026 14:34:23 +0000 (15:34 +0100)
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.

65 files changed:
Content.Client/Administration/UI/BanList/BanListEui.cs
Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs
Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs
Content.Client/Administration/UI/BanList/IBanListLine.cs
Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs
Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs
Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs
Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
Content.IntegrationTests/Tests/Commands/PardonCommand.cs
Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
Content.Server.Database/Model.Ban.cs [new file with mode: 0644]
Content.Server.Database/Model.cs
Content.Server.Database/ModelPostgres.cs
Content.Server.Database/ModelSqlite.cs
Content.Server/Administration/BanList/BanListEui.cs
Content.Server/Administration/BanPanelEui.cs
Content.Server/Administration/Commands/BanCommand.cs
Content.Server/Administration/Commands/BanListCommand.cs
Content.Server/Administration/Commands/DepartmentBanCommand.cs
Content.Server/Administration/Commands/OpenAdminNotesCommand.cs
Content.Server/Administration/Commands/PardonCommand.cs
Content.Server/Administration/Commands/RoleBanCommand.cs
Content.Server/Administration/Commands/RoleBanListCommand.cs
Content.Server/Administration/Managers/BanManager.Notification.cs
Content.Server/Administration/Managers/BanManager.cs
Content.Server/Administration/Managers/IBanManager.cs
Content.Server/Administration/Notes/AdminNotesEui.cs
Content.Server/Administration/Notes/AdminNotesExtensions.cs
Content.Server/Administration/Notes/AdminNotesManager.cs
Content.Server/Administration/Notes/IAdminNotesManager.cs
Content.Server/Administration/PlayerPanelEui.cs
Content.Server/Administration/Systems/BwoinkSystem.cs
Content.Server/Connection/ConnectionManager.cs
Content.Server/Database/BanDef.cs [new file with mode: 0644]
Content.Server/Database/BanMatcher.cs
Content.Server/Database/DatabaseRecords.cs
Content.Server/Database/EFCoreExtensions.cs [new file with mode: 0644]
Content.Server/Database/ServerBanDef.cs [deleted file]
Content.Server/Database/ServerDbBase.cs
Content.Server/Database/ServerDbManager.cs
Content.Server/Database/ServerDbPostgres.cs
Content.Server/Database/ServerDbSqlite.cs
Content.Server/Database/ServerRoleBanDef.cs [deleted file]
Content.Server/Database/ServerRoleUnbanDef.cs [deleted file]
Content.Server/Database/UnbanDef.cs [moved from Content.Server/Database/ServerUnbanDef.cs with 72% similarity]
Content.Server/IP/IPAddressExt.cs
Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs
Content.Shared.Database/Bans.cs [new file with mode: 0644]
Content.Shared/Administration/BanList/BanListEuiState.cs
Content.Shared/Administration/BanList/SharedBan.cs [new file with mode: 0644]
Content.Shared/Administration/BanList/SharedServerBan.cs [deleted file]
Content.Shared/Administration/BanList/SharedServerRoleBan.cs [deleted file]
Content.Shared/Administration/BanList/SharedUnban.cs [moved from Content.Shared/Administration/BanList/SharedServerUnban.cs with 81% similarity]
Content.Shared/Administration/Notes/SharedAdminNote.cs
Content.Shared/Players/MsgRoleBans.cs
Resources/Locale/en-US/job/role-ban-command.ftl
Resources/engineCommandPerms.yml
Tools/dump_user_data.py
Tools/erase_user_data.py

index 2fca1dee5235535724ba35bbdd9545d4ef63c131..00b27cd173f1649ac362b0358f1ae006b7edaa1f 100644 (file)
@@ -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<T>(IBanListLine<T> line, SharedServerBan ban) where T : SharedServerBan
+    public static void SetData<T>(IBanListLine<T> 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<T>(IBanListLine<T> line) where T : SharedServerBan
+    private void OnLineIdsClicked<T>(IBanListLine<T> 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);
 
index 431087568a1dfc0c95f5dddd8986bc990e9468ad..a79fc4a137e0d5255c357d15bb8396e311f73ed3 100644 (file)
@@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control
         RobustXamlLoader.Load(this);
     }
 
-    public void SetBans(List<SharedServerBan> bans)
+    public void SetBans(List<SharedBan> bans)
     {
         for (var i = Bans.ChildCount - 1; i >= 1; i--)
         {
index 0c4e6e60d00b4d5ec1fba5dd75d3d3de0c0337e7..f1320ef7b9963c6b9c35b5e854d809bf6bc27238 100644 (file)
@@ -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<SharedServerBan>
+public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedBan>
 {
-    public SharedServerBan Ban { get; }
+    public SharedBan Ban { get; }
 
     public event Action<BanListLine>? IdsClicked;
 
-    public BanListLine(SharedServerBan ban)
+    public BanListLine(SharedBan ban)
     {
         RobustXamlLoader.Load(this);
 
index 097bae15df7e24d0176b0897f682057dfaec9425..565e707218453d5e5d039cd7c9e4b6adc0e15c7c 100644 (file)
@@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls;
 
 namespace Content.Client.Administration.UI.BanList;
 
-public interface IBanListLine<T> where T : SharedServerBan
+public interface IBanListLine<T> where T : SharedBan
 {
     T Ban { get; }
     Label Reason { get; }
index 1ea751deb7f2144add26bfea06d9bf6a048958f0..f217dec5e6681e566c376cff20f05ef74372be04 100644 (file)
@@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control
         RobustXamlLoader.Load(this);
     }
 
-    public void SetRoleBans(List<SharedServerRoleBan> bans)
+    public void SetRoleBans(List<SharedBan> bans)
     {
         for (var i = RoleBans.ChildCount - 1; i >= 1; i--)
         {
index 4f77d662e1f9eda26d1202f46b236df89e255dae..ca0d214e31e7cd63697fdc85b9bbdc2f691ee507 100644 (file)
@@ -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<SharedServerRoleBan>
+public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedBan>
 {
-    public SharedServerRoleBan Ban { get; }
+    public SharedBan Ban { get; }
 
     public event Action<RoleBanListLine>? IdsClicked;
 
-    public RoleBanListLine(SharedServerRoleBan ban)
+    public RoleBanListLine(SharedBan ban)
     {
         RobustXamlLoader.Load(this);
 
@@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedS
         IdsHidden.OnPressed += IdsPressed;
 
         BanListEui.SetData(this, ban);
-        Role.Text = ban.Role;
+        Role.Text = string.Join(", ", ban.Roles ?? []);
     }
 
     private void IdsPressed(ButtonEventArgs buttonEventArgs)
index 97ddc150007a4e13a4e0b1ef57cd390d638d2289..569b8c9b1bf5d047ea93174705f04ad322be24de 100644 (file)
@@ -70,7 +70,7 @@ public sealed partial class AdminNotesLine : BoxContainer
 
         TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
         ServerLabel.Text = Note.ServerName ?? "Unknown";
-        RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round;
+        RoundLabel.Text = Note.Rounds.Length == 0 ? "Unknown round" : "Round " + string.Join(',', Note.Rounds);
         AdminLabel.Text = Note.CreatedByName;
         PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h";
 
@@ -143,7 +143,12 @@ public sealed partial class AdminNotesLine : BoxContainer
 
     private string FormatRoleBanMessage()
     {
-        var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
+        var rolesText = string.Join(
+            ", ",
+            // Explicit cast here to avoid sandbox violation.
+            (IEnumerable<BanRoleDef>?)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);
     }
 
index 18a50031582d722cc8dd85f3c9510bd37bc85315..e82b85acb6a4657b784bf4d857766661dd250407 100644 (file)
@@ -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));
index d085d9005cdf5757e58d892a4ad5f0c0b53b032b..9325507c536460a7900bb1fe1b4307ce03a3a122 100644 (file)
@@ -25,8 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
     [Dependency] private readonly IPrototypeManager _prototypes = default!;
 
     private readonly Dictionary<string, TimeSpan> _roles = new();
-    private readonly List<string> _jobBans = new();
-    private readonly List<string> _antagBans = new();
+    private readonly List<ProtoId<JobPrototype>> _jobBans = new();
+    private readonly List<ProtoId<AntagPrototype>> _antagBans = new();
     private readonly List<string> _jobWhitelists = new();
 
     private ISawmill _sawmill = default!;
index 9e57cd4b0e6f2995d035b6e200c54166665cdf3a..5f77af1b1045fcd4b4b6be48136008246e5d2563 100644 (file)
@@ -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 (file)
index 0000000..62dde10
--- /dev/null
@@ -0,0 +1,2125 @@
+// <auto-generated />
+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
+    {
+        /// <inheritdoc />
+        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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<bool>("Deadminned")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deadminned");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("boolean")
+                        .HasColumnName("suspended");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("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<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<short>("Impact")
+                        .HasColumnType("smallint")
+                        .HasColumnName("impact");
+
+                    b.Property<JsonDocument>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("message");
+
+                    b.Property<int>("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<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("integer")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_messages_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("boolean")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_notes_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("boolean")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_watchlists_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("antag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("assigned_user_id_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.Property<byte>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_address_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_hwid_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_role_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_template_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<TimeSpan>("Length")
+                        .HasColumnType("interval")
+                        .HasColumnName("length");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.Property<string>("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<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("smallint")
+                        .HasColumnName("denied");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.Property<float>("Trust")
+                        .HasColumnType("real")
+                        .HasColumnName("trust");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ipintel_cache_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<float>("Score")
+                        .HasColumnType("real")
+                        .HasColumnName("score");
+
+                    b.Property<DateTime>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("job_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("integer")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("play_time_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("interval")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<IPAddress>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.PrimitiveCollection<List<string>>("ConstructionFavorites")
+                        .IsRequired()
+                        .HasColumnType("text[]")
+                        .HasColumnName("construction_favorites");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("integer")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Age")
+                        .HasColumnType("integer")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_name");
+
+                    b.Property<JsonDocument>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("integer")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("integer")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("integer")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("EntityName")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)")
+                        .HasColumnName("entity_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("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<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_hit_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("trait_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("bytea")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("integer")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("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<int>("BanHwidId")
+                                .HasColumnType("integer")
+                                .HasColumnName("ban_hwid_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("ConnectionLogId")
+                                .HasColumnType("integer")
+                                .HasColumnName("connection_log_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("PlayerId")
+                                .HasColumnType("integer")
+                                .HasColumnName("player_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("last_seen_hwid");
+
+                            b1.Property<int>("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 (file)
index 0000000..64a20c2
--- /dev/null
@@ -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
+{
+    /// <inheritdoc />
+    public partial class BanRefactor : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "ban",
+                columns: table => new
+                {
+                    ban_id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    type = table.Column<byte>(type: "smallint", nullable: false),
+                    playtime_at_note = table.Column<TimeSpan>(type: "interval", nullable: false),
+                    ban_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+                    expiration_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+                    reason = table.Column<string>(type: "text", nullable: false),
+                    severity = table.Column<int>(type: "integer", nullable: false),
+                    banning_admin = table.Column<Guid>(type: "uuid", nullable: true),
+                    last_edited_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+                    last_edited_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+                    exempt_flags = table.Column<int>(type: "integer", nullable: false),
+                    auto_delete = table.Column<bool>(type: "boolean", nullable: false),
+                    hidden = table.Column<bool>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    address = table.Column<NpgsqlInet>(type: "inet", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    hwid = table.Column<byte[]>(type: "bytea", nullable: false),
+                    hwid_type = table.Column<int>(type: "integer", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    user_id = table.Column<Guid>(type: "uuid", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    role_type = table.Column<string>(type: "text", nullable: false),
+                    role_id = table.Column<string>(type: "text", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    ban_id = table.Column<int>(type: "integer", nullable: false),
+                    round_id = table.Column<int>(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<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    ban_id = table.Column<int>(type: "integer", nullable: false),
+                    unbanning_admin = table.Column<Guid>(type: "uuid", nullable: true),
+                    unban_time = table.Column<DateTime>(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();
+                """);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            throw new NotSupportedException("This migration cannot be rolled back");
+        }
+    }
+}
index 9ab525942c7a712a58ca9baafa5133e453b48d7a..1c85aac946ed08615dd02f486af0c1679d4372b0 100644 (file)
@@ -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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.Property<byte>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_address_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_hwid_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_role_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("integer")
-                        .HasColumnName("server_ban_id");
-
-                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                    b.Property<NpgsqlInet?>("Address")
-                        .HasColumnType("inet")
-                        .HasColumnName("address");
-
-                    b.Property<bool>("AutoDelete")
-                        .HasColumnType("boolean")
-                        .HasColumnName("auto_delete");
-
-                    b.Property<DateTime>("BanTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("ban_time");
-
-                    b.Property<Guid?>("BanningAdmin")
-                        .HasColumnType("uuid")
-                        .HasColumnName("banning_admin");
-
-                    b.Property<int>("ExemptFlags")
-                        .HasColumnType("integer")
-                        .HasColumnName("exempt_flags");
-
-                    b.Property<DateTime?>("ExpirationTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("expiration_time");
-
-                    b.Property<bool>("Hidden")
-                        .HasColumnType("boolean")
-                        .HasColumnName("hidden");
-
-                    b.Property<DateTime?>("LastEditedAt")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("last_edited_at");
-
-                    b.Property<Guid?>("LastEditedById")
-                        .HasColumnType("uuid")
-                        .HasColumnName("last_edited_by_id");
-
-                    b.Property<Guid?>("PlayerUserId")
-                        .HasColumnType("uuid")
-                        .HasColumnName("player_user_id");
-
-                    b.Property<TimeSpan>("PlaytimeAtNote")
-                        .HasColumnType("interval")
-                        .HasColumnName("playtime_at_note");
-
-                    b.Property<string>("Reason")
-                        .IsRequired()
-                        .HasColumnType("text")
-                        .HasColumnName("reason");
-
-                    b.Property<int?>("RoundId")
-                        .HasColumnType("integer")
-                        .HasColumnName("round_id");
-
-                    b.Property<int>("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<Guid>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("integer")
-                        .HasColumnName("server_role_ban_id");
-
-                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                    b.Property<NpgsqlInet?>("Address")
-                        .HasColumnType("inet")
-                        .HasColumnName("address");
-
-                    b.Property<DateTime>("BanTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("ban_time");
-
-                    b.Property<Guid?>("BanningAdmin")
-                        .HasColumnType("uuid")
-                        .HasColumnName("banning_admin");
-
-                    b.Property<DateTime?>("ExpirationTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("expiration_time");
-
-                    b.Property<bool>("Hidden")
-                        .HasColumnType("boolean")
-                        .HasColumnName("hidden");
-
-                    b.Property<DateTime?>("LastEditedAt")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("last_edited_at");
-
-                    b.Property<Guid?>("LastEditedById")
-                        .HasColumnType("uuid")
-                        .HasColumnName("last_edited_by_id");
-
-                    b.Property<Guid?>("PlayerUserId")
-                        .HasColumnType("uuid")
-                        .HasColumnName("player_user_id");
-
-                    b.Property<TimeSpan>("PlaytimeAtNote")
-                        .HasColumnType("interval")
-                        .HasColumnName("playtime_at_note");
-
-                    b.Property<string>("Reason")
-                        .IsRequired()
-                        .HasColumnType("text")
-                        .HasColumnName("reason");
-
-                    b.Property<string>("RoleId")
-                        .IsRequired()
-                        .HasColumnType("text")
-                        .HasColumnName("role_id");
-
-                    b.Property<int?>("RoundId")
-                        .HasColumnType("integer")
-                        .HasColumnName("round_id");
-
-                    b.Property<int>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("integer")
-                        .HasColumnName("role_unban_id");
-
-                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                    b.Property<int>("BanId")
-                        .HasColumnType("integer")
-                        .HasColumnName("ban_id");
-
-                    b.Property<DateTime>("UnbanTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("unban_time");
-
-                    b.Property<Guid?>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("integer")
-                        .HasColumnName("unban_id");
-
-                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
-                    b.Property<int>("BanId")
-                        .HasColumnType("integer")
-                        .HasColumnName("ban_id");
-
-                    b.Property<DateTime>("UnbanTime")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("unban_time");
-
-                    b.Property<Guid?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("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<int>("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<int>("BanHwidId")
+                                .HasColumnType("integer")
+                                .HasColumnName("ban_hwid_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("ServerBanId")
-                                .HasColumnType("integer")
-                                .HasColumnName("server_ban_id");
-
-                            b1.Property<byte[]>("Hwid")
-                                .IsRequired()
-                                .HasColumnType("bytea")
-                                .HasColumnName("hwid");
-
-                            b1.Property<int>("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<int>("ServerRoleBanId")
-                                .HasColumnType("integer")
-                                .HasColumnName("server_role_ban_id");
-
-                            b1.Property<byte[]>("Hwid")
-                                .IsRequired()
-                                .HasColumnType("bytea")
-                                .HasColumnName("hwid");
-
-                            b1.Property<int>("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 (file)
index 0000000..804e3aa
--- /dev/null
@@ -0,0 +1,2044 @@
+// <auto-generated />
+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
+    {
+        /// <inheritdoc />
+        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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<bool>("Deadminned")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deadminned");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("suspended");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_flag_id");
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("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<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<sbyte>("Impact")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("impact");
+
+                    b.Property<string>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<int>("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<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_messages_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_notes_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_watchlists_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("antag_id");
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("assigned_user_id_id");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.Property<byte>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_address_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_hwid_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_player_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_role_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_round_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_template_id");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<TimeSpan>("Length")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("length");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.Property<string>("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<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_log_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("denied");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.Property<float>("Trust")
+                        .HasColumnType("REAL")
+                        .HasColumnName("trust");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ipintel_cache_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<float>("Score")
+                        .HasColumnType("REAL")
+                        .HasColumnName("score");
+
+                    b.Property<DateTime>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("job_id");
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("play_time_id");
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("player_id");
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<string>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.PrimitiveCollection<string>("ConstructionFavorites")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("construction_favorites");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<int>("Age")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_name");
+
+                    b.Property<byte[]>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_id");
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<string>("EntityName")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("entity_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("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<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<string>("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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_hit_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("trait_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("BLOB")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("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<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("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<int>("BanHwidId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("ban_hwid_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("ConnectionLogId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("connection_log_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("PlayerId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("player_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("last_seen_hwid");
+
+                            b1.Property<int>("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 (file)
index 0000000..f813d10
--- /dev/null
@@ -0,0 +1,498 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    /// <inheritdoc />
+    public partial class BanRefactor : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "ban",
+                columns: table => new
+                {
+                    ban_id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    type = table.Column<byte>(type: "INTEGER", nullable: false),
+                    playtime_at_note = table.Column<TimeSpan>(type: "TEXT", nullable: false),
+                    ban_time = table.Column<DateTime>(type: "TEXT", nullable: false),
+                    expiration_time = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    reason = table.Column<string>(type: "TEXT", nullable: false),
+                    severity = table.Column<int>(type: "INTEGER", nullable: false),
+                    banning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
+                    last_edited_by_id = table.Column<Guid>(type: "TEXT", nullable: true),
+                    last_edited_at = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    exempt_flags = table.Column<int>(type: "INTEGER", nullable: false),
+                    auto_delete = table.Column<bool>(type: "INTEGER", nullable: false),
+                    hidden = table.Column<bool>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    address = table.Column<string>(type: "TEXT", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    hwid = table.Column<byte[]>(type: "BLOB", nullable: false),
+                    hwid_type = table.Column<int>(type: "INTEGER", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    user_id = table.Column<Guid>(type: "TEXT", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    role_type = table.Column<string>(type: "TEXT", nullable: false),
+                    role_id = table.Column<string>(type: "TEXT", nullable: false),
+                    ban_id = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    ban_id = table.Column<int>(type: "INTEGER", nullable: false),
+                    round_id = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    ban_id = table.Column<int>(type: "INTEGER", nullable: false),
+                    unbanning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
+                    unban_time = table.Column<DateTime>(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");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            throw new NotSupportedException("This migration cannot be rolled back");
+        }
+    }
+}
index 2d2df5e595d9849cda2388d3ac590fa44b71a875..b7ae8c5d1fbdde378e80d748b28487a627d892a5 100644 (file)
@@ -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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.Property<byte>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_address_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_hwid_id");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_player_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_role_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_round_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("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<int>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("server_ban_id");
-
-                    b.Property<string>("Address")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("address");
-
-                    b.Property<bool>("AutoDelete")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("auto_delete");
-
-                    b.Property<DateTime>("BanTime")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("ban_time");
-
-                    b.Property<Guid?>("BanningAdmin")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("banning_admin");
-
-                    b.Property<int>("ExemptFlags")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("exempt_flags");
-
-                    b.Property<DateTime?>("ExpirationTime")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("expiration_time");
-
-                    b.Property<bool>("Hidden")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("hidden");
-
-                    b.Property<DateTime?>("LastEditedAt")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("last_edited_at");
-
-                    b.Property<Guid?>("LastEditedById")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("last_edited_by_id");
-
-                    b.Property<Guid?>("PlayerUserId")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("player_user_id");
-
-                    b.Property<TimeSpan>("PlaytimeAtNote")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("playtime_at_note");
-
-                    b.Property<string>("Reason")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasColumnName("reason");
-
-                    b.Property<int?>("RoundId")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("round_id");
-
-                    b.Property<int>("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<Guid>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("server_role_ban_id");
-
-                    b.Property<string>("Address")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("address");
-
-                    b.Property<DateTime>("BanTime")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("ban_time");
-
-                    b.Property<Guid?>("BanningAdmin")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("banning_admin");
-
-                    b.Property<DateTime?>("ExpirationTime")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("expiration_time");
-
-                    b.Property<bool>("Hidden")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("hidden");
-
-                    b.Property<DateTime?>("LastEditedAt")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("last_edited_at");
-
-                    b.Property<Guid?>("LastEditedById")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("last_edited_by_id");
-
-                    b.Property<Guid?>("PlayerUserId")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("player_user_id");
-
-                    b.Property<TimeSpan>("PlaytimeAtNote")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("playtime_at_note");
-
-                    b.Property<string>("Reason")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasColumnName("reason");
-
-                    b.Property<string>("RoleId")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasColumnName("role_id");
-
-                    b.Property<int?>("RoundId")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("round_id");
-
-                    b.Property<int>("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<int>("Id")
                         .ValueGeneratedOnAdd()
                         .HasColumnType("INTEGER")
-                        .HasColumnName("role_unban_id");
+                        .HasColumnName("trait_id");
 
-                    b.Property<int>("BanId")
+                    b.Property<int>("ProfileId")
                         .HasColumnType("INTEGER")
-                        .HasColumnName("ban_id");
-
-                    b.Property<DateTime>("UnbanTime")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("unban_time");
+                        .HasColumnName("profile_id");
 
-                    b.Property<Guid?>("UnbanningAdmin")
+                    b.Property<string>("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<int>("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<int>("Id")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("trait_id");
-
-                    b.Property<int>("ProfileId")
-                        .HasColumnType("INTEGER")
-                        .HasColumnName("profile_id");
-
-                    b.Property<string>("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<int>("BanHwidId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("ban_hwid_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("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<int>("ServerBanId")
-                                .HasColumnType("INTEGER")
-                                .HasColumnName("server_ban_id");
-
-                            b1.Property<byte[]>("Hwid")
-                                .IsRequired()
-                                .HasColumnType("BLOB")
-                                .HasColumnName("hwid");
-
-                            b1.Property<int>("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<int>("ServerRoleBanId")
-                                .HasColumnType("INTEGER")
-                                .HasColumnName("server_role_ban_id");
-
-                            b1.Property<byte[]>("Hwid")
-                                .IsRequired()
-                                .HasColumnType("BLOB")
-                                .HasColumnName("hwid");
-
-                            b1.Property<int>("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 (file)
index 0000000..7d1ee2a
--- /dev/null
@@ -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<Ban>()
+            .HasOne(b => b.CreatedBy)
+            .WithMany(pl => pl.AdminServerBansCreated)
+            .HasForeignKey(b => b.BanningAdmin)
+            .HasPrincipalKey(pl => pl.UserId)
+            .OnDelete(DeleteBehavior.SetNull);
+
+        modelBuilder.Entity<Ban>()
+            .HasOne(b => b.LastEditedBy)
+            .WithMany(pl => pl.AdminServerBansLastEdited)
+            .HasForeignKey(b => b.LastEditedById)
+            .HasPrincipalKey(pl => pl.UserId)
+            .OnDelete(DeleteBehavior.SetNull);
+
+        modelBuilder.Entity<BanPlayer>()
+            .HasIndex(bp => new { bp.UserId, bp.BanId })
+            .IsUnique();
+
+        modelBuilder.Entity<BanHwid>()
+            .OwnsOne(bp => bp.HWId)
+            .Property(hwid => hwid.Hwid)
+            .HasColumnName("hwid");
+
+        modelBuilder.Entity<BanRole>()
+            .HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId })
+            .IsUnique();
+
+        modelBuilder.Entity<BanRound>()
+            .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<BanAddress>()
+        //     .HasIndex(bp => new { bp.Address, bp.BanId })
+        //     .IsUnique();
+        // modelBuilder.Entity<BanHwid>()
+        //     .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid })
+        //     .IsUnique();
+        // (postgres only)
+        // modelBuilder.Entity<BanAddress>()
+        //     .HasIndex(ba => ba.Address)
+        //     .IncludeProperties(ba => ba.BanId)
+        //     .IsUnique()
+        //     .HasMethod("gist")
+        //     .HasOperators("inet_ops");
+
+        modelBuilder.Entity<Ban>()
+            .ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0"));
+    }
+}
+
+/// <summary>
+/// Specifies a ban of some kind.
+/// </summary>
+/// <remarks>
+/// <para>
+/// Bans come in two types: <see cref="BanType.Server"/> and <see cref="BanType.Role"/>,
+/// distinguished with <see cref="Type"/>.
+/// </para>
+/// <para>
+/// Bans have one or more "matching data", these being <see cref="BanAddress"/>, <see cref="BanPlayer"/>,
+/// and <see cref="BanHwid"/> entities. If a player's connection info matches any of these,
+/// the ban's effects will apply to that player.
+/// </para>
+/// <para>
+/// 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 <see cref="Unban"/> entity existing for this ban.
+/// </para>
+/// </remarks>
+public sealed class Ban
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// Whether this is a role or server ban.
+    /// </summary>
+    public required BanType Type { get; set; }
+
+    public TimeSpan PlaytimeAtNote { get; set; }
+
+    /// <summary>
+    /// The time when the ban was applied by an administrator.
+    /// </summary>
+    public DateTime BanTime { get; set; }
+
+    /// <summary>
+    /// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
+    /// </summary>
+    public DateTime? ExpirationTime { get; set; }
+
+    /// <summary>
+    /// The administrator-stated reason for applying the ban.
+    /// </summary>
+    public string Reason { get; set; } = null!;
+
+    /// <summary>
+    /// The severity of the incident
+    /// </summary>
+    public NoteSeverity Severity { get; set; }
+
+    /// <summary>
+    /// User ID of the admin that initially applied the ban.
+    /// </summary>
+    [ForeignKey(nameof(CreatedBy))]
+    public Guid? BanningAdmin { get; set; }
+
+    public Player? CreatedBy { get; set; }
+
+    /// <summary>
+    /// User ID of the admin that last edited the note
+    /// </summary>
+    [ForeignKey(nameof(LastEditedBy))]
+    public Guid? LastEditedById { get; set; }
+
+    public Player? LastEditedBy { get; set; }
+    public DateTime? LastEditedAt { get; set; }
+
+    /// <summary>
+    /// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
+    /// </summary>
+    public ServerBanExemptFlags ExemptFlags { get; set; }
+
+    /// <summary>
+    /// Whether this ban should be automatically deleted from the database when it expires.
+    /// </summary>
+    /// <remarks>
+    /// 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 &lt; NOW()"
+    /// </remarks>
+    public bool AutoDelete { get; set; }
+
+    /// <summary>
+    /// Whether to display this ban in the admin remarks (notes) panel
+    /// </summary>
+    public bool Hidden { get; set; }
+
+    /// <summary>
+    /// If present, an administrator has manually repealed this ban.
+    /// </summary>
+    public Unban? Unban { get; set; }
+
+    public List<BanRound>? Rounds { get; set; }
+    public List<BanPlayer>? Players { get; set; }
+    public List<BanAddress>? Addresses { get; set; }
+    public List<BanHwid>? Hwids { get; set; }
+    public List<BanRole>? Roles { get; set; }
+    public List<ServerBanHit>? BanHits { get; set; }
+}
+
+/// <summary>
+/// Base type for entities that specify ban matching data.
+/// </summary>
+public interface IBanSelector
+{
+    int BanId { get; }
+    Ban? Ban { get; }
+}
+
+/// <summary>
+/// Indicates that a ban was related to a round (e.g. placed on that round).
+/// </summary>
+public sealed class BanRound
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// The ID of the ban to which this round was relevant.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    public Ban? Ban { get; set; }
+
+    /// <summary>
+    /// The ID of the round to which this ban was relevant to.
+    /// </summary>
+    [ForeignKey(nameof(Round))]
+    public int RoundId { get; set; }
+
+    public Round? Round { get; set; }
+}
+
+/// <summary>
+/// Specifies a player that a <see cref="T:Database.Ban"/> matches.
+/// </summary>
+public sealed class BanPlayer : IBanSelector
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// The user ID of the banned player.
+    /// </summary>
+    public Guid UserId { get; set; }
+
+    /// <summary>
+    /// The ID of the ban to which this applies.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    public Ban? Ban { get; set; }
+}
+
+/// <summary>
+/// Specifies an IP address range that a <see cref="T:Database.Ban"/> matches.
+/// </summary>
+public sealed class BanAddress : IBanSelector
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// The address range being matched.
+    /// </summary>
+    public required NpgsqlInet Address { get; set; }
+
+    /// <summary>
+    /// The ID of the ban to which this applies.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    public Ban? Ban { get; set; }
+}
+
+/// <summary>
+/// Specifies a HWID that a <see cref="T:Database.Ban"/> matches.
+/// </summary>
+public sealed class BanHwid : IBanSelector
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// The HWID being matched.
+    /// </summary>
+    public required TypedHwid HWId { get; set; }
+
+    /// <summary>
+    /// The ID of the ban to which this applies.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    public Ban? Ban { get; set; }
+}
+
+/// <summary>
+/// A single role banned among a greater role ban record.
+/// </summary>
+/// <remarks>
+/// <see cref="Ban"/>s of type <see cref="BanType.Role"/> should have one or more <see cref="BanRole"/>s
+/// to store which roles are actually banned.
+/// It is invalid for <see cref="BanType.Server"/> bans to have <see cref="BanRole"/> entities.
+/// </remarks>
+public sealed class BanRole
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// What type of role is being banned. For example <c>Job</c> or <c>Antag</c>.
+    /// </summary>
+    public required string RoleType { get; set; }
+
+    /// <summary>
+    /// The ID of the role being banned. This is probably something like a prototype.
+    /// </summary>
+    public required string RoleId { get; set; }
+
+    /// <summary>
+    /// The ID of the ban to which this applies.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    public Ban? Ban { get; set; }
+}
+
+/// <summary>
+/// An explicit repeal of a <see cref="Ban"/> by an administrator.
+/// Having an entry for a ban neutralizes it.
+/// </summary>
+public sealed class Unban
+{
+    public int Id { get; set; }
+
+    /// <summary>
+    /// The ID of ban that is being repealed.
+    /// </summary>
+    [ForeignKey(nameof(Ban))]
+    public int BanId { get; set; }
+
+    /// <summary>
+    /// The ban that is being repealed.
+    /// </summary>
+    public Ban? Ban { get; set; }
+
+    /// <summary>
+    /// The admin that repealed the ban.
+    /// </summary>
+    public Guid? UnbanningAdmin { get; set; }
+
+    /// <summary>
+    /// The time the ban was repealed.
+    /// </summary>
+    public DateTime UnbanTime { get; set; }
+}
index ac5b003a737d0a21aea9fe55f4f9c9801396bf12..f54bba7e44c88b16b9a82f300d6808b8c52ec5ba 100644 (file)
@@ -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> AdminLogPlayer { get; set; } = null!;
         public DbSet<Whitelist> Whitelist { get; set; } = null!;
         public DbSet<Blacklist> Blacklist { get; set; } = null!;
-        public DbSet<ServerBan> Ban { get; set; } = default!;
-        public DbSet<ServerUnban> Unban { get; set; } = default!;
+        public DbSet<Ban> Ban { get; set; } = default!;
+        public DbSet<BanRound> BanRound { get; set; } = default!;
+        public DbSet<BanPlayer> BanPlayer { get; set; } = default!;
+        public DbSet<BanAddress> BanAddress { get; set; } = default!;
+        public DbSet<BanHwid> BanHwid { get; set; } = default!;
+        public DbSet<BanRole> BanRole { get; set; } = default!;
+        public DbSet<Unban> Unban { get; set; } = default!;
         public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
         public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!;
         public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
-        public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
-        public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
+
         public DbSet<PlayTime> PlayTime { get; set; } = default!;
         public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
         public DbSet<AdminNote> AdminNotes { get; set; } = null!;
@@ -145,43 +148,11 @@ namespace Content.Server.Database
             modelBuilder.Entity<AdminLogPlayer>()
                 .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
 
-            modelBuilder.Entity<ServerBan>()
-                .HasIndex(p => p.PlayerUserId);
-
-            modelBuilder.Entity<ServerBan>()
-                .HasIndex(p => p.Address);
-
-            modelBuilder.Entity<ServerBan>()
-                .HasIndex(p => p.PlayerUserId);
-
-            modelBuilder.Entity<ServerUnban>()
-                .HasIndex(p => p.BanId)
-                .IsUnique();
-
-            modelBuilder.Entity<ServerBan>().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<ServerBanExemption>().ToTable(t =>
                 t.HasCheckConstraint("FlagsNotZero", "flags != 0"));
 
-            modelBuilder.Entity<ServerRoleBan>()
-                .HasIndex(p => p.PlayerUserId);
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .HasIndex(p => p.Address);
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .HasIndex(p => p.PlayerUserId);
-
-            modelBuilder.Entity<ServerRoleUnban>()
-                .HasIndex(p => p.BanId)
-                .IsUnique();
-
-            modelBuilder.Entity<ServerRoleBan>().ToTable(t =>
-                t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
-
             modelBuilder.Entity<Player>()
                 .HasIndex(p => p.UserId)
                 .IsUnique();
@@ -296,34 +267,6 @@ namespace Content.Server.Database
                 t.HasCheckConstraint("NotDismissedAndSeen",
                     "NOT dismissed OR seen"));
 
-            modelBuilder.Entity<ServerBan>()
-                .HasOne(ban => ban.CreatedBy)
-                .WithMany(author => author.AdminServerBansCreated)
-                .HasForeignKey(ban => ban.BanningAdmin)
-                .HasPrincipalKey(author => author.UserId)
-                .OnDelete(DeleteBehavior.SetNull);
-
-            modelBuilder.Entity<ServerBan>()
-                .HasOne(ban => ban.LastEditedBy)
-                .WithMany(author => author.AdminServerBansLastEdited)
-                .HasForeignKey(ban => ban.LastEditedById)
-                .HasPrincipalKey(author => author.UserId)
-                .OnDelete(DeleteBehavior.SetNull);
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .HasOne(ban => ban.CreatedBy)
-                .WithMany(author => author.AdminServerRoleBansCreated)
-                .HasForeignKey(ban => ban.BanningAdmin)
-                .HasPrincipalKey(author => author.UserId)
-                .OnDelete(DeleteBehavior.SetNull);
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .HasOne(ban => ban.LastEditedBy)
-                .WithMany(author => author.AdminServerRoleBansLastEdited)
-                .HasForeignKey(ban => ban.LastEditedById)
-                .HasPrincipalKey(author => author.UserId)
-                .OnDelete(DeleteBehavior.SetNull);
-
             modelBuilder.Entity<RoleWhitelist>()
                 .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<ServerBan>()
-                .OwnsOne(p => p.HWId)
-                .Property(p => p.Hwid)
-                .HasColumnName("hwid");
-
-            modelBuilder.Entity<ServerBan>()
-                .OwnsOne(p => p.HWId)
-                .Property(p => p.Type)
-                .HasDefaultValue(HwidType.Legacy);
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .OwnsOne(p => p.HWId)
-                .Property(p => p.Hwid)
-                .HasColumnName("hwid");
-
-            modelBuilder.Entity<ServerRoleBan>()
-                .OwnsOne(p => p.HWId)
-                .Property(p => p.Type)
-                .HasDefaultValue(HwidType.Legacy);
-
             modelBuilder.Entity<ConnectionLog>()
                 .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<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -591,10 +516,8 @@ namespace Content.Server.Database
         public List<AdminMessage> AdminMessagesCreated { get; set; } = null!;
         public List<AdminMessage> AdminMessagesLastEdited { get; set; } = null!;
         public List<AdminMessage> AdminMessagesDeleted { get; set; } = null!;
-        public List<ServerBan> AdminServerBansCreated { get; set; } = null!;
-        public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
-        public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
-        public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
+        public List<Ban> AdminServerBansCreated { get; set; } = null!;
+        public List<Ban> AdminServerBansLastEdited { get; set; } = null!;
         public List<RoleWhitelist> 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<TUnban> 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; }
-    }
-
     /// <summary>
     /// Flags for use with <see cref="ServerBanExemption"/>.
     /// </summary>
@@ -785,138 +684,6 @@ namespace Content.Server.Database
         // @formatter:on
     }
 
-    /// <summary>
-    /// 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.
-    /// </summary>
-    /// <remarks>
-    /// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing).
-    /// </remarks>
-    [Table("server_ban"), Index(nameof(PlayerUserId))]
-    public class ServerBan : IBanCommon<ServerUnban>
-    {
-        public int Id { get; set; }
-
-        [ForeignKey("Round")]
-        public int? RoundId { get; set; }
-        public Round? Round { get; set; }
-
-        /// <summary>
-        /// The user ID of the banned player.
-        /// </summary>
-        public Guid? PlayerUserId { get; set; }
-        [Required] public TimeSpan PlaytimeAtNote { get; set; }
-
-        /// <summary>
-        /// CIDR IP address range of the ban. The whole range can match the ban.
-        /// </summary>
-        public NpgsqlInet? Address { get; set; }
-
-        /// <summary>
-        /// Hardware ID of the banned player.
-        /// </summary>
-        public TypedHwid? HWId { get; set; }
-
-        /// <summary>
-        /// The time when the ban was applied by an administrator.
-        /// </summary>
-        public DateTime BanTime { get; set; }
-
-        /// <summary>
-        /// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
-        /// </summary>
-        public DateTime? ExpirationTime { get; set; }
-
-        /// <summary>
-        /// The administrator-stated reason for applying the ban.
-        /// </summary>
-        public string Reason { get; set; } = null!;
-
-        /// <summary>
-        /// The severity of the incident
-        /// </summary>
-        public NoteSeverity Severity { get; set; }
-
-        /// <summary>
-        /// User ID of the admin that applied the ban.
-        /// </summary>
-        [ForeignKey("CreatedBy")]
-        public Guid? BanningAdmin { get; set; }
-
-        public Player? CreatedBy { get; set; }
-
-        /// <summary>
-        /// User ID of the admin that last edited the note
-        /// </summary>
-        [ForeignKey("LastEditedBy")]
-        public Guid? LastEditedById { get; set; }
-
-        public Player? LastEditedBy { get; set; }
-
-        /// <summary>
-        /// When the ban was last edited
-        /// </summary>
-        public DateTime? LastEditedAt { get; set; }
-
-        /// <summary>
-        /// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
-        /// </summary>
-        public ServerBanExemptFlags ExemptFlags { get; set; }
-
-        /// <summary>
-        /// If present, an administrator has manually repealed this ban.
-        /// </summary>
-        public ServerUnban? Unban { get; set; }
-
-        /// <summary>
-        /// Whether this ban should be automatically deleted from the database when it expires.
-        /// </summary>
-        /// <remarks>
-        /// 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 &lt; NOW()"
-        /// </remarks>
-        public bool AutoDelete { get; set; }
-
-        /// <summary>
-        /// Whether to display this ban in the admin remarks (notes) panel
-        /// </summary>
-        public bool Hidden { get; set; }
-
-        public List<ServerBanHit> BanHits { get; set; } = null!;
-    }
-
-    /// <summary>
-    /// An explicit repeal of a <see cref="ServerBan"/> by an administrator.
-    /// Having an entry for a ban neutralizes it.
-    /// </summary>
-    [Table("server_unban")]
-    public class ServerUnban : IUnbanCommon
-    {
-        [Column("unban_id")] public int Id { get; set; }
-
-        /// <summary>
-        /// The ID of ban that is being repealed.
-        /// </summary>
-        public int BanId { get; set; }
-
-        /// <summary>
-        /// The ban that is being repealed.
-        /// </summary>
-        public ServerBan Ban { get; set; } = null!;
-
-        /// <summary>
-        /// The admin that repealed the ban.
-        /// </summary>
-        public Guid? UnbanningAdmin { get; set; }
-
-        /// <summary>
-        /// The time the ban repealed.
-        /// </summary>
-        public DateTime UnbanTime { get; set; }
-    }
-
     /// <summary>
     /// An exemption for a specific user to a certain type of <see cref="ServerBan"/>.
     /// </summary>
@@ -937,7 +704,7 @@ namespace Content.Server.Database
 
         /// <summary>
         /// The ban flags to exempt this player from.
-        /// If any bit overlaps <see cref="ServerBan.ExemptFlags"/>, the ban is ignored.
+        /// If any bit overlaps <see cref="Ban.ExemptFlags"/>, the ban is ignored.
         /// </summary>
         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<ServerRoleUnban>
-    {
-        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
         /// <summary>
         /// The reason for the ban.
         /// </summary>
-        /// <seealso cref="ServerBan.Reason"/>
+        /// <seealso cref="Ban.Reason"/>
         public string Reason { get; set; } = "";
 
         /// <summary>
         /// Exemptions granted to the ban.
         /// </summary>
-        /// <seealso cref="ServerBan.ExemptFlags"/>
+        /// <seealso cref="Ban.ExemptFlags"/>
         public ServerBanExemptFlags ExemptFlags { get; set; }
 
         /// <summary>
         /// Severity of the ban
         /// </summary>
-        /// <seealso cref="ServerBan.Severity"/>
+        /// <seealso cref="Ban.Severity"/>
         public NoteSeverity Severity { get; set; }
 
         /// <summary>
         /// Ban will be automatically deleted once expired.
         /// </summary>
-        /// <seealso cref="ServerBan.AutoDelete"/>
+        /// <seealso cref="Ban.AutoDelete"/>
         public bool AutoDelete { get; set; }
 
         /// <summary>
         /// Ban is not visible to players in the remarks menu.
         /// </summary>
-        /// <seealso cref="ServerBan.Hidden"/>
+        /// <seealso cref="Ban.Hidden"/>
         public bool Hidden { get; set; }
     }
 
index 7499d0b0f5956974e795b48651a10e8e2c27ccf1..c3f41b7058edd83b711516a45dc8488a4ead38de 100644 (file)
@@ -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<ServerBan>().ToTable(t =>
-                t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
-
-            modelBuilder.Entity<ServerRoleBan>().ToTable( t =>
+            modelBuilder.Entity<BanAddress>().ToTable(t =>
                 t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
 
             modelBuilder.Entity<Player>().ToTable(t =>
index 33bc2bcd729757a85e45895c70dfb4b815cf571c..a64a2af8d9cb1fc0edff8c104b6a14bfdb585487 100644 (file)
@@ -58,13 +58,7 @@ namespace Content.Server.Database
             );
 
             modelBuilder
-                .Entity<ServerBan>()
-                .Property(e => e.Address)
-                .HasColumnType("TEXT")
-                .HasConversion(ipMaskConverter);
-
-            modelBuilder
-                .Entity<ServerRoleBan>()
+                .Entity<BanAddress>()
                 .Property(e => e.Address)
                 .HasColumnType("TEXT")
                 .HasConversion(ipMaskConverter);
index 2ca126bf16418c84fb02ae035e74af122c547e11..549a14f67334381fc7c8bce71c57d470fae48ba9 100644 (file)
@@ -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<SharedServerBan> Bans { get; } = new();
-    private List<SharedServerRoleBan> RoleBans { get; } = new();
+    private List<SharedBan> Bans { get; } = new();
+    private List<SharedBan> 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<SharedBan> 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<string> 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();
     }
index 4a4b7218729b3bce2056948907bc98bbaf67aef6..fceb2b5750482804f432b39efa29a27966b78565 100644 (file)
@@ -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();
     }
index f76cfded814e774804dc513b5101419a15d91207..a6d3b1064622eb2e7d51a3c40db3063e6dd90a78 100644 (file)
@@ -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)
index ea68788deb59a24d7b76f38ed0c78c67dc738bd8..26a8647248acf64537f9380b3515b0177226796f 100644 (file)
@@ -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)
             {
index 15f9859ca10e7d6e450a687bd1e316cb391b63e4..40faeb74048b482da5d4dddbeab391dc8adc6295 100644 (file)
@@ -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)
index 5577e134371ab5855cb766e1b8faa4c7ccd5a632..35a6d1096aaf35b6384fa28e7ff495c748886a36 100644 (file)
@@ -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)
index 5c4417a966f4888df0059b1ae4302b222ef2f99a..eb292663e2e881fa47f44c517b2514f801c74d52 100644 (file)
@@ -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)));
         }
index c49af32881b8ae08aea9547cc38fe3aba2390db6..f303f31b868a0e2f8316184109bd23b6f5f9650f 100644 (file)
@@ -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<JobPrototype>(role))
-            _bans.CreateRoleBan<JobPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
+        {
+            banInfo.AddJob(new ProtoId<JobPrototype>(role));
+        }
         else if (_proto.HasIndex<AntagPrototype>(role))
-            _bans.CreateRoleBan<AntagPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
+        {
+            banInfo.AddAntag(new ProtoId<AntagPrototype>(role));
+        }
         else
+        {
             shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role)));
+            return;
+        }
+
+        _bans.CreateRoleBan(banInfo);
     }
 
     public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
index 8244ded3b20913faa8062982ee01ae1ba7245363..4abd406cbc055eae81bdf884811d81c3f85ce06a 100644 (file)
@@ -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;
index ff84887f00df129fdbe98b5c872eba76b26f74b8..d627dc508f1fb2546c2eca83d3e7c76c70ad2530 100644 (file)
@@ -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
         /// </summary>
         [JsonRequired, JsonPropertyName("ban_id")]
         public int BanId { get; init; }
-
-        /// <summary>
-        /// The id of the server the ban was made on.
-        /// This is used to avoid double work checking the ban on the originating server.
-        /// </summary>
-        /// <remarks>
-        /// This is optional in case the ban was made outside a server (SS14.Admin)
-        /// </remarks>
-        [JsonPropertyName("server_id")]
-        public int? ServerId { get; init; }
     }
 }
index 17f796e699ca01b90aeaa11fc063279d6dde1ebf..ccf76e3995da07addfe8f6a9d9f86c9ea0a9dcaa 100644 (file)
@@ -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<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
+    private readonly Dictionary<ICommonSession, List<BanDef>> _cachedRoleBans = new();
     // Cached ban exemption flags are used to handle
     private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
 
@@ -72,9 +72,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
         var netChannel = player.Channel;
         ImmutableArray<byte>? 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<ServerRoleBanDef>();
+        var roleBans = await _db.GetBansAsync(
+            netChannel.RemoteEndPoint.Address,
+            player.UserId,
+            hwId,
+            modernHwids,
+            false,
+            type: BanType.Role);
+
+        var userRoleBans = new List<BanDef>();
         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<GameTicker>(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<string> 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<T>(
-        NetUserId? target,
-        string? targetUsername,
-        NetUserId? banningAdmin,
-        (IPAddress, int)? addressRange,
-        ImmutableTypedHwid? hwid,
-        ProtoId<T> role,
-        uint? minutes,
-        NoteSeverity severity,
-        string reason,
-        DateTimeOffset timeOfBan
-    ) where T : class, IPrototype
+    public async void CreateRoleBan(CreateRoleBanInfo banInfo)
     {
-        string encodedRole;
+        ImmutableArray<BanRoleDef> 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<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(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<JobPrototype>(role))
-            encodedRole = PrefixJob + role;
-        else if (_prototypeManager.HasIndex<AntagPrototype>(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<BanRoleDef>? 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<int> roundIds;
+        if (banInfo.RoundIds.Count > 0)
+        {
+            roundIds = [..banInfo.RoundIds];
+        }
+        else if (_systems.TryGetEntitySystem<GameTicker>(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<TimeSpan> 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<BanRoleDef> ToBanRoleDef<T>(IEnumerable<ProtoId<T>> 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<JobPrototype>(protoId) && _prototypeManager.HasIndex<AntagPrototype>(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<T>(), protoId);
+        });
+    }
+
+    private static string PrototypeKindToDbType<T>() 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<bool> 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<string> 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<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
     {
-        return GetRoleBans<JobPrototype>(playerUserId, PrefixJob);
+        return GetRoleBans<JobPrototype>(playerUserId);
     }
 
     public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
     {
-        return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag);
+        return GetRoleBans<AntagPrototype>(playerUserId);
     }
 
-    private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype
+    private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId) where T : class, IPrototype
     {
         if (!_playerManager.TryGetSessionById(playerUserId, out var session))
             return null;
 
-        return GetRoleBans<T>(session, prefix);
+        return GetRoleBans<T>(session);
     }
 
-    private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype
+    private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession) where T : class, IPrototype
     {
         if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
             return null;
 
+        var dbType = PrototypeKindToDbType<T>();
+
         return roleBans
-            .Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
-            .Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
+            .SelectMany(ban => ban.Roles!.Value)
+            .Where(role => role.RoleType == dbType)
+            .Select(role => new ProtoId<T>(role.RoleId))
             .ToHashSet();
     }
 
-    public HashSet<string>? GetRoleBans(NetUserId playerUserId)
+    public HashSet<BanRoleDef>? 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<ProtoId<JobPrototype>> jobs)
     {
-        return IsRoleBanned(player, jobs, PrefixJob);
+        return IsRoleBanned<JobPrototype>(player, jobs);
     }
 
     public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
     {
-        return IsRoleBanned(player, antags, PrefixAntag);
+        return IsRoleBanned<AntagPrototype>(player, antags);
     }
 
-    private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype
+    private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles) where T : class, IPrototype
     {
         var bans = GetRoleBans(player.UserId);
 
         if (bans is null || bans.Count == 0)
             return false;
 
+        var dbType = PrototypeKindToDbType<T>();
+
         // 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<JobPrototype>(pSession, PrefixJob);
-        var jobBansList = new List<string>(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<AntagPrototype>(pSession, PrefixAntag);
-        var antagBansList = new List<string>(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<JobPrototype>(pSession) ?? []).ToList(),
+            AntagBans = (GetRoleBans<AntagPrototype>(pSession) ?? []).ToList(),
         };
 
         _sawmill.Debug($"Sent role bans to {pSession.Name}");
index 1912ebe9ecd04dbe7d3e665d55b31931b19b5155..633ae968dba25e63853975b8d7ee12031612b0ed 100644 (file)
@@ -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();
 
+    /// <summary>
+    /// Create a server ban in the database, blocking connection for matching players.
+    /// </summary>
+    void CreateServerBan(CreateServerBanInfo banInfo);
+
     /// <summary>
     /// Bans the specified target, address range and / or HWID. One of them must be non-null
     /// </summary>
@@ -23,12 +29,44 @@ public interface IBanManager
     /// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
     /// <param name="severity">Severity of the resulting ban note</param>
     /// <param name="reason">Reason for the ban</param>
-    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);
+    }
 
     /// <summary>
     /// Gets a list of prefixed prototype IDs with the player's role bans.
     /// </summary>
-    public HashSet<string>? GetRoleBans(NetUserId playerUserId);
+    public HashSet<BanRoleDef>? GetRoleBans(NetUserId playerUserId);
 
     /// <summary>
     /// Checks if the player is currently banned from any of the listed roles.
@@ -57,33 +95,12 @@ public interface IBanManager
     public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
 
     /// <summary>
-    /// Creates a job ban for the specified target, username or GUID
+    /// Creates a role ban, preventing matching players from playing said roles.
     /// </summary>
-    /// <param name="target">Target user, username or GUID, null for none</param>
-    /// <param name="targetUsername">The username of the target, if known</param>
-    /// <param name="banningAdmin">The responsible admin for the ban</param>
-    /// <param name="addressRange">The range of IPs that are to be banned, if known</param>
-    /// <param name="hwid">The HWID to be banned, if known</param>
-    /// <param name="role">The role ID to be banned from. Either an AntagPrototype or a JobPrototype</param>
-    /// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
-    /// <param name="severity">Severity of the resulting ban note</param>
-    /// <param name="reason">Reason for the ban</param>
-    /// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
-    public void CreateRoleBan<T>(
-        NetUserId? target,
-        string? targetUsername,
-        NetUserId? banningAdmin,
-        (IPAddress, int)? addressRange,
-        ImmutableTypedHwid? hwid,
-        ProtoId<T> role,
-        uint? minutes,
-        NoteSeverity severity,
-        string reason,
-        DateTimeOffset timeOfBan
-    ) where T : class, IPrototype;
+    public void CreateRoleBan(CreateRoleBanInfo banInfo);
 
     /// <summary>
-    /// Pardons a role ban for the specified target, username or GUID
+    /// Pardons a role ban by its ID.
     /// </summary>
     /// <param name="banId">The id of the role ban to pardon.</param>
     /// <param name="unbanningAdmin">The admin, if any, that pardoned the role ban.</param>
@@ -96,3 +113,287 @@ public interface IBanManager
     /// <param name="pSession">Player's session</param>
     public void SendRoleBans(ICommonSession pSession);
 }
+
+/// <summary>
+/// Base info to fill out in created ban records.
+/// </summary>
+/// <seealso cref="CreateServerBanInfo"/>
+/// <seealso cref="CreateRoleBanInfo"/>
+[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<ImmutableTypedHwid> HWIds = [];
+    internal readonly HashSet<int> RoundIds = [];
+    internal TimeSpan? Duration;
+    internal NoteSeverity? Severity;
+    internal string Reason;
+    internal NetUserId? BanningAdmin;
+
+    protected CreateBanInfo(string reason)
+    {
+        Reason = reason;
+    }
+
+    /// <summary>
+    /// Add a user to be matched by the ban.
+    /// </summary>
+    /// <remarks>
+    /// Bans can target multiple users at once.
+    /// </remarks>
+    /// <param name="userId">The ID of the user.</param>
+    /// <param name="username">The name of the user (used for logging purposes).</param>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddUser(NetUserId userId, string username)
+    {
+        Users.Add((userId, username));
+        return this;
+    }
+
+    /// <summary>
+    /// Add an IP address to be matched by the ban.
+    /// </summary>
+    /// <remarks>
+    /// Bans can target multiple addresses at once.
+    /// </remarks>
+    /// <param name="address">
+    /// The IP address to add. If null, nothing is done.
+    /// </param>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddAddress(IPAddress? address)
+    {
+        if (address == null)
+            return this;
+
+        return AddAddressRange(
+            address,
+            address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6);
+    }
+
+    /// <summary>
+    /// Add an IP address range to be matched by the ban.
+    /// </summary>
+    /// <remarks>
+    /// Bans can target multiple address ranges at once.
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange)
+    {
+        return AddAddressRange(addressRange.Address, addressRange.Mask);
+    }
+
+    /// <summary>
+    /// Add an IP address range to be matched by the ban.
+    /// </summary>
+    /// <remarks>
+    /// Bans can target multiple address ranges at once.
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddAddressRange(IPAddress address, int mask)
+    {
+        AddressRanges.Add((address, mask));
+        return this;
+    }
+
+    /// <summary>
+    /// Add a hardware IP (HWID) to be matched by the ban.
+    /// </summary>
+    /// <remarks>
+    /// Bans can target multiple HWIDs at once.
+    /// </remarks>
+    /// <param name="hwId">
+    /// The HWID to add. If null, nothing is done.
+    /// </param>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId)
+    {
+        if (hwId != null)
+            HWIds.Add(hwId);
+
+        return this;
+    }
+
+    /// <summary>
+    /// Add a relevant round ID to this ban.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// If not specified, the current round ID is used for the ban.
+    /// Therefore, the first call to this function will <i>replace</i> the round ID,
+    /// and further calls will add additional round IDs.
+    /// </para>
+    /// <para>
+    /// Bans can target multiple round IDs at once.
+    /// </para>
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo AddRoundId(int roundId)
+    {
+        RoundIds.Add(roundId);
+        return this;
+    }
+
+    /// <summary>
+    /// Set how long the ban will last, in minutes.
+    /// </summary>
+    /// <remarks>
+    /// If no duration is specified, the ban is permanent.
+    /// </remarks>
+    /// <param name="minutes">The duration of the ban, in minutes.</param>
+    /// <returns>The current object, for easy chaining.</returns>
+    /// <exception cref="ArgumentOutOfRangeException">
+    /// Thrown if <see cref="minutes"/> is not a positive number.
+    /// </exception>
+    public CreateBanInfo WithMinutes(int minutes)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
+        return WithMinutes((uint)minutes);
+    }
+
+    /// <summary>
+    /// Set how long the ban will last, in minutes.
+    /// </summary>
+    /// <remarks>
+    /// If no duration is specified, the ban is permanent.
+    /// </remarks>
+    /// <param name="minutes">The duration of the ban, in minutes.</param>
+    /// <returns>The current object, for easy chaining.</returns>
+    /// <exception cref="ArgumentOutOfRangeException">
+    /// Thrown if <see cref="minutes"/> is not a positive number.
+    /// </exception>
+    public CreateBanInfo WithMinutes(uint minutes)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
+        return WithDuration(TimeSpan.FromMinutes(minutes));
+    }
+
+    /// <summary>
+    /// Set how long the ban will last.
+    /// </summary>
+    /// <remarks>
+    /// If no duration is specified, the ban is permanent.
+    /// </remarks>
+    /// <param name="duration">The duration of the ban.</param>
+    /// <returns>The current object, for easy chaining.</returns>
+    /// <exception cref="ArgumentOutOfRangeException">
+    /// Thrown if <see cref="duration"/> is not a positive amount of time.
+    /// </exception>
+    public CreateBanInfo WithDuration(TimeSpan duration)
+    {
+        if (duration <= TimeSpan.Zero)
+            throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero.");
+
+        Duration = duration;
+        return this;
+    }
+
+    /// <summary>
+    /// Set the severity of the ban.
+    /// </summary>
+    /// <remarks>
+    /// If no severity is specified, the default is specified through server configuration.
+    /// </remarks>
+    /// <param name="severity"></param>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo WithSeverity(NoteSeverity severity)
+    {
+        Severity = severity;
+        return this;
+    }
+
+    /// <summary>
+    /// Set the reason for the ban.
+    /// </summary>
+    /// <remarks>
+    /// This replaces the value given via the object constructor.
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo WithReason(string reason)
+    {
+        Reason = reason;
+        return this;
+    }
+
+    /// <summary>
+    /// Specify the admin responsible for placing the ban.
+    /// </summary>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin)
+    {
+        BanningAdmin = banningAdmin;
+        return this;
+    }
+}
+
+/// <summary>
+/// Stores info to create server ban records.
+/// </summary>
+/// <seealso cref="IBanManager.CreateServerBan(CreateServerBanInfo)"/>
+[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
+public sealed class CreateServerBanInfo : CreateBanInfo
+{
+    /// <param name="reason">The reason for the server ban.</param>
+    public CreateServerBanInfo(string reason) : base(reason)
+    {
+    }
+}
+
+/// <summary>
+/// Stores info to create role ban records.
+/// </summary>
+/// <seealso cref="IBanManager.CreateRoleBan(CreateRoleBanInfo)"/>
+[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
+public sealed class CreateRoleBanInfo : CreateBanInfo
+{
+    internal readonly HashSet<ProtoId<AntagPrototype>> AntagPrototypes = [];
+    internal readonly HashSet<ProtoId<JobPrototype>> JobPrototypes = [];
+
+    /// <param name="reason">The reason for the role ban.</param>
+    public CreateRoleBanInfo(string reason) : base(reason)
+    {
+    }
+
+    /// <summary>
+    /// Add an antag role that will be unavailable for banned players.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// Bans can have multiple roles at once.
+    /// </para>
+    /// <para>
+    /// While not checked in this function, adding a ban with invalid role IDs will cause a
+    /// <see cref="UnknownPrototypeException"/> when actually creating the ban.
+    /// </para>
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateRoleBanInfo AddAntag(ProtoId<AntagPrototype> protoId)
+    {
+        AntagPrototypes.Add(protoId);
+        return this;
+    }
+
+    /// <summary>
+    /// Add a job role that will be unavailable for banned players.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// Bans can have multiple roles at once.
+    /// </para>
+    /// <para>
+    /// While not checked in this function, adding a ban with invalid role IDs will cause a
+    /// <see cref="UnknownPrototypeException"/> when actually creating the ban.
+    /// </para>
+    /// </remarks>
+    /// <returns>The current object, for easy chaining.</returns>
+    public CreateRoleBanInfo AddJob(ProtoId<JobPrototype> protoId)
+    {
+        JobPrototypes.Add(protoId);
+        return this;
+    }
+}
index d1297b251d77840f7822a5ae5c03b45b23ec3b37..5ecb9c774dc97f75e805850c4c28931193f1a23b 100644 (file)
@@ -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));
index 349c7ff3bdfbd54915e2328055bd9510dd88ef0c..e2ec62ed6181859a05c037ab395822c1b8192d1e 100644 (file)
@@ -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<BanRoleDef>? 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,
index 412b1911710da8a9f861ef586785630feac26694..f10cd4e3f9c4c07e1dc3f81846c9b381f735d7d8 100644 (file)
@@ -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");
index f54f8a21bdab9c609979ae63660769dfc3760a01..4e992ba30b1ce860d70565b3c94d23b9ab35bdbb 100644 (file)
@@ -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);
index 7de62ac74376191faa48421ddd0bd1d713191628..13a5d42a4ed46900d35ec3f1d3f08fc2c0f99215 100644 (file)
@@ -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
         {
index 91211716b574700f00d96081f7effe9ee75df751..172e27ee8082217347955c53ea724b2e461954ce 100644 (file)
@@ -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));
index 9e6ba89d91ba8d2d303c814cd475182fbd1f5fff..c3a389621c4c2f102ae6f346b06f7272101d4708 100644 (file)
@@ -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<ServerBanDef>? bansHit)?> ShouldDeny(
+        private async Task<(ConnectionDenyReason, string, List<BanDef>? 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 (file)
index 0000000..d459b47
--- /dev/null
@@ -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<NetUserId> UserIds { get; }
+        public ImmutableArray<(IPAddress address, int cidrMask)> Addresses { get; }
+        public ImmutableArray<ImmutableTypedHwid> HWIds { get; }
+
+        public DateTimeOffset BanTime { get; }
+        public DateTimeOffset? ExpirationTime { get; }
+        public ImmutableArray<int> 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<BanRoleDef>? Roles { get; }
+
+        public BanDef(
+            int? id,
+            BanType type,
+            ImmutableArray<NetUserId> userIds,
+            ImmutableArray<(IPAddress address, int cidrMask)> addresses,
+            ImmutableArray<ImmutableTypedHwid> hwIds,
+            DateTimeOffset banTime,
+            DateTimeOffset? expirationTime,
+            ImmutableArray<int> roundIds,
+            TimeSpan playtimeAtNote,
+            string reason,
+            NoteSeverity severity,
+            NetUserId? banningAdmin,
+            UnbanDef? unban,
+            ServerBanExemptFlags exemptFlags = default,
+            ImmutableArray<BanRoleDef>? 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")}
+                   """;
+        }
+    }
+}
index f477ccd822a6db13109152bb6de233c7f10bed0d..0302c0dc13a3c17131baebf9913c9a2c070cff12 100644 (file)
@@ -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;
 
 /// <summary>
-/// Implements logic to match a <see cref="ServerBanDef"/> against a player query.
+/// Implements logic to match a <see cref="BanDef"/> against a player query.
 /// </summary>
 /// <remarks>
 /// <para>
@@ -29,7 +30,7 @@ public static class BanMatcher
     /// <param name="ban">The ban information.</param>
     /// <param name="player">Information about the player to match against.</param>
     /// <returns>True if the ban matches the provided player info.</returns>
-    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;
index 30fba3434b8991c9fd40beebcb84f65762aba711..63ab45a726b13016c62d6129a55a5dbcb6838bb3 100644 (file)
@@ -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<RoundRecord> Rounds { get; }
 
-    public PlayerRecord? Player { get; }
+    public ImmutableArray<PlayerRecord> 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<RoundRecord> Rounds,
+    ImmutableArray<PlayerRecord> 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<BanRoleDef> 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<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
+    ImmutableArray<PlayerRecord> 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<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
+    ImmutableArray<PlayerRecord> 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<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
+    ImmutableArray<PlayerRecord> 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 (file)
index 0000000..58dbc46
--- /dev/null
@@ -0,0 +1,37 @@
+using System.Linq;
+using System.Linq.Expressions;
+using Microsoft.EntityFrameworkCore;
+
+namespace Content.Server.Database;
+
+internal static class EFCoreExtensions
+{
+    extension<TEntity>(IQueryable<TEntity> query) where TEntity : class
+    {
+        public IQueryable<TEntity> ApplyIncludes(
+            IEnumerable<Expression<Func<TEntity, object>>> properties)
+        {
+            var q = query;
+            foreach (var property in properties)
+            {
+                q = q.Include(property);
+            }
+
+            return q;
+        }
+
+        public IQueryable<TEntity> ApplyIncludes<TDerived>(
+            IEnumerable<Expression<Func<TDerived, object>>> properties,
+            Expression<Func<TEntity, TDerived>> 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 (file)
index a09f9e9..0000000
+++ /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")}
-                   """;
-        }
-    }
-}
index 00ad726d503706916a137b5d28d48b0662fb1450..2c5524f5023d153f6c7a10e817ca029dc564050f 100644 (file)
@@ -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
         /// </summary>
         /// <param name="id">The ban id to look for.</param>
         /// <returns>The ban with the given id or null if none exist.</returns>
-        public abstract Task<ServerBanDef?> GetServerBanAsync(int id);
+        public abstract Task<BanDef?> GetBanAsync(int id);
 
         /// <summary>
         ///     Looks up an user's most recent received un-pardoned ban.
@@ -469,11 +469,12 @@ namespace Content.Server.Database
         /// <param name="hwId">The legacy HWId of the user.</param>
         /// <param name="modernHWIds">The modern HWIDs of the user.</param>
         /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
-        public abstract Task<ServerBanDef?> GetServerBanAsync(
+        public abstract Task<BanDef?> GetBanAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds);
+            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
+            BanType type);
 
         /// <summary>
         ///     Looks up an user's ban history.
@@ -486,17 +487,18 @@ namespace Content.Server.Database
         /// <param name="modernHWIds">The modern HWIDs of the user.</param>
         /// <param name="includeUnbanned">Include pardoned and expired bans.</param>
         /// <returns>The user's ban history.</returns>
-        public abstract Task<List<ServerBanDef>> GetServerBansAsync(
+        public abstract Task<List<BanDef>> GetBansAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned);
+            bool includeUnbanned,
+            BanType type);
 
-        public abstract Task AddServerBanAsync(ServerBanDef serverBan);
-        public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
+        public abstract Task<BanDef> 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
-         */
-        /// <summary>
-        ///     Looks up a role ban by id.
-        ///     This will return a pardoned role ban as well.
-        /// </summary>
-        /// <param name="id">The role ban id to look for.</param>
-        /// <returns>The role ban with the given id or null if none exist.</returns>
-        public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
-
-        /// <summary>
-        ///     Looks up an user's role ban history.
-        ///     This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
-        ///     Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
-        /// </summary>
-        /// <param name="address">The IP address of the user.</param>
-        /// <param name="userId">The NetUserId of the user.</param>
-        /// <param name="hwId">The Hardware Id of the user.</param>
-        /// <param name="modernHWIds">The modern HWIDs of the user.</param>
-        /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
-        /// <returns>The user's role ban history.</returns>
-        public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned);
-
-        public abstract Task<ServerRoleBanDef> 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<Expression<Func<Ban, object>>> 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<Expression<Func<Ban, object>>> 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<ServerBanDef> bans)
+        public async Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> 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<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
+        public async Task<BanNoteRecord?> 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<ServerRoleBanNoteRecord?> 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<List<IAdminRemarksRecord>> 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<List<IAdminRemarksRecord>> 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<List<ServerBanNoteRecord>> GetServerBansAsNotesForUser(DbGuard db, Guid user)
+        private static IQueryable<Ban> 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<ServerBanNoteRecord>();
-            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<BanNoteRecord> 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<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
+        // These two are here because they get converted into notes later
+        protected async Task<List<BanNoteRecord>> 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<ServerRoleBanNoteRecord> bans = new();
-            var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
-            foreach (var banGroup in bansEnumerable)
+            var banNotes = new List<BanNoteRecord>();
+            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<IEnumerable<TResult>> AsyncSelect<T, TResult>(
+            IEnumerable<T>? enumerable,
+            Func<T, Task<TResult>> selector)
+        {
+            var results = new List<TResult>();
+
+            foreach (var item in enumerable ?? [])
+            {
+                results.Add(await selector(item));
+            }
+
+            return [..results];
+        }
     }
 }
index 5110227b967693bc5dd260dff5666a57ad3d3593..9231ef5d753ca824b1ef8e5c16c5bf6ebcbdfc4c 100644 (file)
@@ -67,7 +67,7 @@ namespace Content.Server.Database
         /// </summary>
         /// <param name="id">The ban id to look for.</param>
         /// <returns>The ban with the given id or null if none exist.</returns>
-        Task<ServerBanDef?> GetServerBanAsync(int id);
+        Task<BanDef?> GetBanAsync(int id);
 
         /// <summary>
         ///     Looks up an user's most recent received un-pardoned ban.
@@ -79,11 +79,12 @@ namespace Content.Server.Database
         /// <param name="hwId">The legacy HWID of the user.</param>
         /// <param name="modernHWIds">The modern HWIDs of the user.</param>
         /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
-        Task<ServerBanDef?> GetServerBanAsync(
+        Task<BanDef?> GetBanAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds);
+            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
+            BanType type = BanType.Server);
 
         /// <summary>
         ///     Looks up an user's ban history.
@@ -95,17 +96,18 @@ namespace Content.Server.Database
         /// <param name="modernHWIds">The modern HWIDs of the user.</param>
         /// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
         /// <returns>The user's ban history.</returns>
-        Task<List<ServerBanDef>> GetServerBansAsync(
+        Task<List<BanDef>> GetBansAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned=true);
+            bool includeUnbanned=true,
+            BanType type = BanType.Server);
 
-        Task AddServerBanAsync(ServerBanDef serverBan);
-        Task AddServerUnbanAsync(ServerUnbanDef serverBan);
+        Task<BanDef> 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
-        /// <summary>
-        ///     Looks up a role ban by id.
-        ///     This will return a pardoned role ban as well.
-        /// </summary>
-        /// <param name="id">The role ban id to look for.</param>
-        /// <returns>The role ban with the given id or null if none exist.</returns>
-        Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
-
-        /// <summary>
-        ///     Looks up an user's role ban history.
-        ///     This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
-        ///     Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
-        /// </summary>
-        /// <param name="address">The IP address of the user.</param>
-        /// <param name="userId">The NetUserId of the user.</param>
-        /// <param name="hwId">The Hardware Id of the user.</param>
-        /// <param name="modernHWIds">The modern HWIDs of the user.</param>
-        /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
-        /// <returns>The user's role ban history.</returns>
-        Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
-            IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned = true);
-
-        Task<ServerRoleBanDef> 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
 
         /// <summary>
@@ -209,7 +172,7 @@ namespace Content.Server.Database
             ConnectionDenyReason? denied,
             int serverId);
 
-        Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans);
+        Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> bans);
 
         #endregion
 
@@ -301,8 +264,7 @@ namespace Content.Server.Database
         Task<AdminNoteRecord?> GetAdminNote(int id);
         Task<AdminWatchlistRecord?> GetAdminWatchlist(int id);
         Task<AdminMessageRecord?> GetAdminMessage(int id);
-        Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id);
-        Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id);
+        Task<BanNoteRecord?> GetBanAsNoteAsync(int id);
         Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
         Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player);
         Task<List<AdminWatchlistRecord>> 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);
 
         /// <summary>
         /// 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<ServerBanDef?> GetServerBanAsync(int id)
+        public Task<BanDef?> GetBanAsync(int id)
         {
             DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetServerBanAsync(id));
+            return RunDbCommand(() => _db.GetBanAsync(id));
         }
 
-        public Task<ServerBanDef?> GetServerBanAsync(
+        public Task<BanDef?> GetBanAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds)
+            ImmutableArray<ImmutableArray<byte>>? 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<List<ServerBanDef>> GetServerBansAsync(
+        public Task<List<BanDef>> GetBansAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? 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<BanDef> 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<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
-        {
-            DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetServerRoleBanAsync(id));
-        }
-
-        public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
-            IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned = true)
-        {
-            DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
-        }
-
-        public Task<ServerRoleBanDef> 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<List<PlayTime>> 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<ServerBanDef> bans)
+        public Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> bans)
         {
             DbWriteOpsMetric.Inc();
             return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans));
@@ -928,16 +854,10 @@ namespace Content.Server.Database
             return RunDbCommand(() => _db.GetAdminMessage(id));
         }
 
-        public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
+        public Task<BanNoteRecord?> GetBanAsNoteAsync(int id)
         {
             DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id));
-        }
-
-        public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
-        {
-            DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
+            return RunDbCommand(() => _db.GetBanAsNoteAsync(id));
         }
 
         public Task<List<IAdminRemarksRecord>> 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)
index 31584a8d74aff2aeaf4a86691667f93f7b4056eb..f0c56b72951330796a0b72fcec2725e78742884e 100644 (file)
@@ -62,24 +62,26 @@ namespace Content.Server.Database
         }
 
         #region Ban
-        public override async Task<ServerBanDef?> GetServerBanAsync(int id)
+        public override async Task<BanDef?> 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<ServerBanDef?> GetServerBanAsync(
+        public override async Task<BanDef?> GetBanAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds)
+            ImmutableArray<ImmutableArray<byte>>? 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<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
+        public override async Task<List<BanDef>> GetBansAsync(IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? 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<ServerBanDef>(queryBans.Length);
+            var bans = new List<BanDef>(queryBans.Length);
 
             foreach (var ban in queryBans)
             {
@@ -131,7 +133,8 @@ namespace Content.Server.Database
             return bans;
         }
 
-        private static IQueryable<ServerBan> MakeBanLookupQuery(
+        // This has to return IDs instead of direct objects because otherwise all the includes are too complicated.
+        private static IQueryable<Ban> MakeBanLookupQuery(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? 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<ServerBan, ServerUnban>(
-                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<TBan>? MakeBanLookupQualityShared<TBan, TUnban>(
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            DbSet<TBan> set)
-            where TBan : class, IBanCommon<TUnban>
-            where TUnban : class, IUnbanCommon
-        {
-            IQueryable<TBan>? query = null;
+            var selectorQueries = new List<IQueryable<IBanSelector>>();
 
             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<ServerRoleBanDef?> 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<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? 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<List<ServerRoleBanDef>> QueryRoleBans(IQueryable<ServerRoleBan> query)
-        {
-            var queryRoleBans = await query.ToArrayAsync();
-            var bans = new List<ServerRoleBanDef>(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<ServerRoleBan> MakeRoleBanLookupQuery(
-            IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            DbGuardImpl db,
-            bool includeUnbanned)
-        {
-            var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
-                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<BanRoleDef>? 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<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
+        public override async Task<BanDef> 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();
index 3e69ece7f17b0d228b13894ee3b83e7cc4e43632..6bb1bea45fbca1ae98c41a216d760758141179f6 100644 (file)
@@ -70,48 +70,52 @@ namespace Content.Server.Database
         }
 
         #region Ban
-        public override async Task<ServerBanDef?> GetServerBanAsync(int id)
+        public override async Task<BanDef?> 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<ServerBanDef?> GetServerBanAsync(
+        public override async Task<BanDef?> GetBanAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds)
+            ImmutableArray<ImmutableArray<byte>>? 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<List<ServerBanDef>> GetServerBansAsync(
+        public override async Task<List<BanDef>> GetBansAsync(
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? 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<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
+        private async Task<IEnumerable<BanDef>> GetBanQueryAsync(
             DbGuardImpl db,
             IPAddress? address,
             NetUserId? userId,
             ImmutableArray<byte>? hwId,
             ImmutableArray<ImmutableArray<byte>>? 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<List<ServerBan>> GetAllBans(
-            SqliteServerDbContext db,
+        private static async Task<List<Ban>> GetAllBans(SqliteServerDbContext db,
             bool includeUnbanned,
-            ServerBanExemptFlags? exemptFlags)
+            ServerBanExemptFlags? exemptFlags,
+            BanType type)
         {
-            IQueryable<ServerBan> 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<ServerRoleBanDef?> 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<List<ServerRoleBanDef>> GetServerRoleBansAsync(
-            IPAddress? address,
-            NetUserId? userId,
-            ImmutableArray<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? modernHWIds,
-            bool includeUnbanned)
+        public override async Task<BanDef> 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<List<ServerRoleBan>> GetAllRoleBans(
-            SqliteServerDbContext db,
-            bool includeUnbanned)
-        {
-            IQueryable<ServerRoleBan> 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<byte>? hwId,
-            ImmutableArray<ImmutableArray<byte>>? 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<ServerRoleBanDef> 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<BanRoleDef>? 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 (file)
index dda3a82..0000000
+++ /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 (file)
index 3960a86..0000000
+++ /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;
-    }
-}
similarity index 72%
rename from Content.Server/Database/ServerUnbanDef.cs
rename to Content.Server/Database/UnbanDef.cs
index 3d39a6b90c42caca5618b97b8eab2dd57a0e3ff2..1fa4fc2a6a90db2fe96864c10b3e346345b7456e 100644 (file)
@@ -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;
index a61477e01bceb591148dd443597e17f2964c8b04..8c514e96b916b63cd59a94ddf5f8818a255d97d4 100644 (file)
@@ -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
index 614a54e0f6f832d03f9603b5ece65c41257b84a2..a34c90c363c53ff18a235084e36a3a83cc69f833 100644 (file)
@@ -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 (file)
index 0000000..9fb100f
--- /dev/null
@@ -0,0 +1,33 @@
+namespace Content.Shared.Database;
+
+/// <summary>
+/// Types of bans that can be stored in the database.
+/// </summary>
+public enum BanType : byte
+{
+    /// <summary>
+    /// A ban from the entire server. If a player matches the ban info, they will be refused connection.
+    /// </summary>
+    Server,
+
+    /// <summary>
+    /// A ban from playing one or more roles.
+    /// </summary>
+    Role,
+}
+
+/// <summary>
+/// A single role for a database role ban.
+/// </summary>
+/// <param name="RoleType">The type of role being banned, e.g. <c>Job</c>.</param>
+/// <param name="RoleId">
+/// The ID of the role being banned. This is likely a prototype ID based on <paramref name="RoleType"/>.
+/// </param>
+[Serializable]
+public record struct BanRoleDef(string RoleType, string RoleId)
+{
+    public override string ToString()
+    {
+        return $"{RoleType}:{RoleId}";
+    }
+}
index 09faa9706ea31bbdb99635872449eb907c568e57..c885ff1f706ef5b7fdf404c64dae713874958184 100644 (file)
@@ -6,7 +6,7 @@ namespace Content.Shared.Administration.BanList;
 [Serializable, NetSerializable]
 public sealed class BanListEuiState : EuiStateBase
 {
-    public BanListEuiState(string banListPlayerName, List<SharedServerBan> bans, List<SharedServerRoleBan> roleBans)
+    public BanListEuiState(string banListPlayerName, List<SharedBan> bans, List<SharedBan> roleBans)
     {
         BanListPlayerName = banListPlayerName;
         Bans = bans;
@@ -14,6 +14,6 @@ public sealed class BanListEuiState : EuiStateBase
     }
 
     public string BanListPlayerName { get; }
-    public List<SharedServerBan> Bans { get; }
-    public List<SharedServerRoleBan> RoleBans { get; }
+    public List<SharedBan> Bans { get; }
+    public List<SharedBan> RoleBans { get; }
 }
diff --git a/Content.Shared/Administration/BanList/SharedBan.cs b/Content.Shared/Administration/BanList/SharedBan.cs
new file mode 100644 (file)
index 0000000..68ccc02
--- /dev/null
@@ -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<NetUserId> UserIds,
+    ImmutableArray<(string address, int cidrMask)> Addresses,
+    ImmutableArray<string> HWIds,
+    DateTime BanTime,
+    DateTime? ExpirationTime,
+    string Reason,
+    string? BanningAdminName,
+    SharedUnban? Unban,
+    ImmutableArray<BanRoleDef>? Roles);
diff --git a/Content.Shared/Administration/BanList/SharedServerBan.cs b/Content.Shared/Administration/BanList/SharedServerBan.cs
deleted file mode 100644 (file)
index a8b9ce0..0000000
+++ /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 (file)
index fca2ea1..0000000
+++ /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);
similarity index 81%
rename from Content.Shared/Administration/BanList/SharedServerUnban.cs
rename to Content.Shared/Administration/BanList/SharedUnban.cs
index f3a57e415902fa0411588fc1e0dd9a650c6b2444..d60bb9184e2f683fea05eb0513b8c61fa0930d02 100644 (file)
@@ -3,7 +3,7 @@
 namespace Content.Shared.Administration.BanList;
 
 [Serializable, NetSerializable]
-public sealed record SharedServerUnban(
+public sealed record SharedUnban(
     string? UnbanningAdmin,
     DateTime UnbanTime
 );
index 09d4f3f9478fbc1e62c515681cfca90c04a5892c..d849d35078429b49203b153df46372d1dee097a4 100644 (file)
@@ -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<NetUserId> Players, // Notes player
+    ImmutableArray<int> 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<BanRoleDef>? 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?
index bcc28d01d2e89d942ccadddbec4d3ff5ef0dec7f..e6fc2df3fc7b34f4b028671b5855b4bc06079775 100644 (file)
@@ -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<string> JobBans = new();
-    public List<string> AntagBans = new();
+    public List<ProtoId<JobPrototype>> JobBans = new();
+    public List<ProtoId<AntagPrototype>> AntagBans = new();
 
     public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
     {
index 26062c25b7fe90a5dfc24cd69b940cc333e3ded4..148fb9a7b07a3b5780a3f4ba74ed3857fcc7d3c0 100644 (file)
@@ -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
index 4520a1da2d69f53c9b225ba2f2cd3f0e2fd553a4..e5fda48f253c70f2ef9720fd3177d47eead3d3dd 100644 (file)
   - fuck
   - replay_recording_start
   - replay_recording_stop
+  - transfer_test
 
 - Flags: QUERY
   Commands:
index 09f9410805431a82fa539f2e32e166efdfec400b..23b2a7523f33d1e1d03194c24ca89348c8319151 100755 (executable)
@@ -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)
 
 
index 509654f91e8d8fb400d0f27b3292454da782e6dd..dda9042cf70a61d6cf49692aba3318cb1ccbc583 100644 (file)
@@ -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...")