From a00f0a3587dabb473dad714dba3d4d9996bdcb4a Mon Sep 17 00:00:00 2001 From: Livyathan Date: Tue, 24 Mar 2026 14:15:11 -0400 Subject: [PATCH 1/7] re-enable surgery accessibility for cyborgs --- .../Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 81f0af1f54c..4294984a34d 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -74,8 +74,7 @@ type: SurgeryBui - type: ActivatableUI key: enum.BorgUiKey.Key - - type: Targeting # Shitmed Change - # - type: SurgeryTarget # Sandwich: Borgs are silicon, not surgery targets + - type: Targeting # Shitmed - type: SiliconLawBound - type: ActionGrant actions: @@ -86,7 +85,7 @@ - type: SiliconLawProvider laws: Crewsimov - type: Inventory - templateId: borgnorm # Hardlight. Edits regular borgs to use our Inventories. + templateId: borgnorm # Hardlight: Edits regular borgs to use our inventories. - type: Hands showInHands: false disableExplosionRecursion: true From ac8dce1d02cb29239d345cbc8b11f13eed918dc9 Mon Sep 17 00:00:00 2001 From: IrkallaEpsilon <42441793+IrkallaEpsilon@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:19:17 +0100 Subject: [PATCH 2/7] Makes non-marring cats psyker capable (#1052) --- Resources/Prototypes/_EE/Entities/Mobs/Player/tajaran.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Resources/Prototypes/_EE/Entities/Mobs/Player/tajaran.yml b/Resources/Prototypes/_EE/Entities/Mobs/Player/tajaran.yml index 9a810033ca5..c90f3238243 100644 --- a/Resources/Prototypes/_EE/Entities/Mobs/Player/tajaran.yml +++ b/Resources/Prototypes/_EE/Entities/Mobs/Player/tajaran.yml @@ -3,3 +3,5 @@ name: Urist McTajaran parent: MobTajaranBase id: MobTajaran + components: + - type: PotentialPsionic #HL -Irkalla Edit- Makes Bugs Psyker-Capable From 6a62ff5dd77433e418bb734c9ac289b6a79588f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nik=C3=B6?= <76795775+Nikode16@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:19:35 -0500 Subject: [PATCH 3/7] Communication Fix (#1053) [ Fixes the communication console to be a general command access, adds the Engineering 101 Guidebook to all station Engineering Lockers, and updates the PA being mentioned in how to properly set it up and troubleshoot niche reasons it refuses to connect together ] --- .../Entities/Structures/Machines/Computers/computers.yml | 2 +- Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml | 2 ++ Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 1bfa5e9c302..4a4b9e6de09 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -737,7 +737,7 @@ - map: [ "enum.WiresVisualLayers.MaintenancePanel" ] state: generic_panel_open - type: AccessReader - access: [["HeadOfPersonnel"], ["HeadOfSecurity"]] + access: [["Command"]] - type: CommunicationsConsole title: comms-console-announcement-title-station global: true #SR WILL NOT BE IGNORED diff --git a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml index 2ec31f74b35..59b9cd6d327 100644 --- a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml +++ b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml @@ -63,6 +63,7 @@ children: - id: RCD - id: RCDAmmo + - id: BoxFolderEngiForms - type: entity id: HLLockerEngineerFilledHardsuit @@ -101,3 +102,4 @@ - id: RCD - id: RCDAmmo - id: MetalFoamGrenade + - id: BoxFolderEngiForms \ No newline at end of file diff --git a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml index 849cab6eff6..eda3942bd68 100644 --- a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml @@ -172,6 +172,7 @@ - id: ShipyardRCDAmmo - id: trayScanner - id: CEIDCard + - id: BoxFolderEngiForms # Hardsuit table, used for suit storage as well - type: entityTable From c2173cd148eb13d708fa13a26d4b37caf1d7814f Mon Sep 17 00:00:00 2001 From: fenndragon Date: Wed, 25 Mar 2026 17:07:55 -0600 Subject: [PATCH 4/7] Required refactor (#1054) * 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. * screens --------- Co-authored-by: Pieter-Jan Briers --- .../Administration/UI/BanList/BanListEui.cs | 17 +- .../UI/BanList/Bans/BanListControl.xaml.cs | 2 +- .../UI/BanList/Bans/BanListLine.xaml.cs | 6 +- .../Administration/UI/BanList/IBanListLine.cs | 2 +- .../RoleBans/RoleBanListControl.xaml.cs | 2 +- .../BanList/RoleBans/RoleBanListLine.xaml.cs | 8 +- .../UI/Notes/AdminNotesLine.xaml.cs | 8 +- .../UI/Notes/AdminNotesLinePopup.xaml.cs | 4 +- .../JobRequirementsManager.cs | 16 +- .../Tests/Commands/PardonCommand.cs | 40 +- .../20260120200503_BanRefactor.Designer.cs | 2125 +++++++++++++++++ .../Postgres/20260120200503_BanRefactor.cs | 535 +++++ .../PostgresServerDbContextModelSnapshot.cs | 804 ++++--- .../20260120200455_BanRefactor.Designer.cs | 2044 ++++++++++++++++ .../Sqlite/20260120200455_BanRefactor.cs | 498 ++++ .../SqliteServerDbContextModelSnapshot.cs | 724 +++--- Content.Server.Database/Model.Ban.cs | 328 +++ Content.Server.Database/Model.cs | 315 +-- Content.Server.Database/ModelPostgres.cs | 5 +- Content.Server.Database/ModelSqlite.cs | 8 +- .../Administration/BanList/BanListEui.cs | 80 +- Content.Server/Administration/BanPanelEui.cs | 48 +- .../Administration/Commands/BanCommand.cs | 10 +- .../Administration/Commands/BanListCommand.cs | 2 +- .../Commands/DepartmentBanCommand.cs | 15 +- .../Commands/OpenAdminNotesCommand.cs | 3 +- .../Administration/Commands/PardonCommand.cs | 4 +- .../Commands/PrisonerCommands.cs | 10 +- .../Administration/Commands/RoleBanCommand.cs | 16 +- .../Commands/RoleBanListCommand.cs | 10 +- .../Managers/BanManager.Notification.cs | 18 +- .../Administration/Managers/BanManager.cs | 401 ++-- .../Administration/Managers/IBanManager.cs | 367 ++- .../Administration/Notes/AdminNotesEui.cs | 8 +- .../Notes/AdminNotesExtensions.cs | 18 +- .../Administration/Notes/AdminNotesManager.cs | 25 +- .../Notes/IAdminNotesManager.cs | 3 +- .../Administration/PlayerPanelEui.cs | 7 +- .../Administration/Systems/BwoinkSystem.cs | 2 +- .../Connection/ConnectionManager.cs | 4 +- Content.Server/Database/BanDef.cs | 128 + Content.Server/Database/BanMatcher.cs | 50 +- Content.Server/Database/DatabaseRecords.cs | 53 +- Content.Server/Database/EFCoreExtensions.cs | 38 + Content.Server/Database/ServerBanDef.cs | 94 - Content.Server/Database/ServerDbBase.cs | 330 +-- Content.Server/Database/ServerDbManager.cs | 154 +- Content.Server/Database/ServerDbPostgres.cs | 379 +-- Content.Server/Database/ServerDbSqlite.cs | 298 +-- Content.Server/Database/ServerRoleBanDef.cs | 65 - Content.Server/Database/ServerRoleUnbanDef.cs | 19 - .../{ServerUnbanDef.cs => UnbanDef.cs} | 4 +- Content.Server/IP/IPAddressExt.cs | 16 +- .../Managers/VoteManager.DefaultVotes.cs | 19 +- Content.Shared.Database/Bans.cs | 33 + .../Administration/BanList/BanListEuiState.cs | 6 +- .../Administration/BanList/SharedBan.cs | 20 + .../Administration/BanList/SharedServerBan.cs | 17 - .../BanList/SharedServerRoleBan.cs | 18 - .../{SharedServerUnban.cs => SharedUnban.cs} | 2 +- .../Administration/Notes/SharedAdminNote.cs | 7 +- Content.Shared/Players/MsgRoleBans.cs | 30 +- .../Locale/en-US/job/role-ban-command.ftl | 1 - .../computer_wallmount_apt.png | Bin 0 -> 1437 bytes .../Machines/computer_wallmount.rsi/meta.json | 50 +- .../screen_occupied.png | Bin 0 -> 765 bytes .../computer_wallmount.rsi/screen_vacant.png | Bin 0 -> 788 bytes Resources/engineCommandPerms.yml | 1 + Tools/dump_user_data.py | 38 +- Tools/erase_user_data.py | 24 +- 70 files changed, 7840 insertions(+), 2596 deletions(-) create mode 100644 Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs create mode 100644 Content.Server.Database/Model.Ban.cs create mode 100644 Content.Server/Database/BanDef.cs create mode 100644 Content.Server/Database/EFCoreExtensions.cs delete mode 100644 Content.Server/Database/ServerBanDef.cs delete mode 100644 Content.Server/Database/ServerRoleBanDef.cs delete mode 100644 Content.Server/Database/ServerRoleUnbanDef.cs rename Content.Server/Database/{ServerUnbanDef.cs => UnbanDef.cs} (72%) create mode 100644 Content.Shared.Database/Bans.cs create mode 100644 Content.Shared/Administration/BanList/SharedBan.cs delete mode 100644 Content.Shared/Administration/BanList/SharedServerBan.cs delete mode 100644 Content.Shared/Administration/BanList/SharedServerRoleBan.cs rename Content.Shared/Administration/BanList/{SharedServerUnban.cs => SharedUnban.cs} (81%) create mode 100644 Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/computer_wallmount_apt.png create mode 100644 Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_occupied.png create mode 100644 Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_vacant.png mode change 100755 => 100644 Tools/dump_user_data.py diff --git a/Content.Client/Administration/UI/BanList/BanListEui.cs b/Content.Client/Administration/UI/BanList/BanListEui.cs index 2fca1dee523..00b27cd173f 100644 --- a/Content.Client/Administration/UI/BanList/BanListEui.cs +++ b/Content.Client/Administration/UI/BanList/BanListEui.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using Content.Client.Administration.UI.BanList.Bans; using Content.Client.Administration.UI.BanList.RoleBans; using Content.Client.Eui; @@ -73,7 +74,7 @@ private static string FormatDate(DateTimeOffset date) return date.ToString("MM/dd/yyyy h:mm tt"); } - public static void SetData(IBanListLine line, SharedServerBan ban) where T : SharedServerBan + public static void SetData(IBanListLine line, SharedBan ban) where T : SharedBan { line.Reason.Text = ban.Reason; line.BanTime.Text = FormatDate(ban.BanTime); @@ -94,20 +95,20 @@ public static void SetData(IBanListLine line, SharedServerBan ban) where T line.BanningAdmin.Text = ban.BanningAdminName; } - private void OnLineIdsClicked(IBanListLine line) where T : SharedServerBan + private void OnLineIdsClicked(IBanListLine line) where T : SharedBan { _popup?.Close(); _popup = null; var ban = line.Ban; var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value)); - var ip = ban.Address == null + var ip = ban.Addresses.Length == 0 ? string.Empty - : Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address)); - var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId)); - var guid = ban.UserId == null + : Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address)))); + var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds))); + var guid = ban.UserIds.Length == 0 ? string.Empty - : Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString())); + : Loc.GetString("ban-list-guid", ("guid", string.Join(',', ban.UserIds))); _popup = new BanListIdsPopup(id, ip, hwid, guid); diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs index 431087568a1..a79fc4a137e 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs @@ -16,7 +16,7 @@ public BanListControl() RobustXamlLoader.Load(this); } - public void SetBans(List bans) + public void SetBans(List bans) { for (var i = Bans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs index 0c4e6e60d00..f1320ef7b99 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs @@ -7,13 +7,13 @@ namespace Content.Client.Administration.UI.BanList.Bans; [GenerateTypedNameReferences] -public sealed partial class BanListLine : BoxContainer, IBanListLine +public sealed partial class BanListLine : BoxContainer, IBanListLine { - public SharedServerBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public BanListLine(SharedServerBan ban) + public BanListLine(SharedBan ban) { RobustXamlLoader.Load(this); diff --git a/Content.Client/Administration/UI/BanList/IBanListLine.cs b/Content.Client/Administration/UI/BanList/IBanListLine.cs index 097bae15df7..565e7072184 100644 --- a/Content.Client/Administration/UI/BanList/IBanListLine.cs +++ b/Content.Client/Administration/UI/BanList/IBanListLine.cs @@ -3,7 +3,7 @@ namespace Content.Client.Administration.UI.BanList; -public interface IBanListLine where T : SharedServerBan +public interface IBanListLine where T : SharedBan { T Ban { get; } Label Reason { get; } diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs index 1ea751deb7f..f217dec5e66 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs @@ -16,7 +16,7 @@ public RoleBanListControl() RobustXamlLoader.Load(this); } - public void SetRoleBans(List bans) + public void SetRoleBans(List bans) { for (var i = RoleBans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs index 4f77d662e1f..ca0d214e31e 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs @@ -7,13 +7,13 @@ namespace Content.Client.Administration.UI.BanList.RoleBans; [GenerateTypedNameReferences] -public sealed partial class RoleBanListLine : BoxContainer, IBanListLine +public sealed partial class RoleBanListLine : BoxContainer, IBanListLine { - public SharedServerRoleBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public RoleBanListLine(SharedServerRoleBan ban) + public RoleBanListLine(SharedBan ban) { RobustXamlLoader.Load(this); @@ -21,7 +21,7 @@ public RoleBanListLine(SharedServerRoleBan ban) IdsHidden.OnPressed += IdsPressed; BanListEui.SetData(this, ban); - Role.Text = ban.Role; + Role.Text = string.Join(", ", ban.Roles ?? []); } private void IdsPressed(ButtonEventArgs buttonEventArgs) diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs index ead1d8b00e5..5779403b646 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs @@ -70,7 +70,7 @@ private void Refresh() 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"; @@ -139,7 +139,11 @@ private string FormatBanMessage() private string FormatRoleBanMessage() { - var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} "); + var rolesText = Note.BannedRoles is { Length: > 0 } + ? string.Join(", ", (IEnumerable) Note.BannedRoles) + : "unknown"; + + var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} "); return FormatBanMessageCommon(banMessage); } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs index 18a50031582..e82b85acb6a 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs @@ -32,9 +32,9 @@ public AdminNotesLinePopup(SharedAdminNote note, string playerName, bool showDel IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id)); TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType)); SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None)); - RoundIdLabel.Text = note.Round == null + RoundIdLabel.Text = note.Rounds.Length == 0 ? Loc.GetString("admin-notes-round-id-unknown") - : Loc.GetString("admin-notes-round-id", ("id", note.Round)); + : Loc.GetString("admin-notes-round-id", ("id", string.Join(',', note.Rounds))); CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName)); CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName)); diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index e82aae21ba6..a7710cacc5a 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -26,7 +26,8 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager [Dependency] private readonly IPrototypeManager _prototypes = default!; private readonly Dictionary _roles = new(); - private readonly List _roleBans = new(); + private readonly List> _jobBans = new(); + private readonly List> _antagBans = new(); private readonly List _jobWhitelists = new(); private ISawmill _sawmill = default!; @@ -53,16 +54,19 @@ private void ClientOnRunLevelChanged(object? sender, RunLevelChangedEventArgs e) // Reset on disconnect, just in case. _roles.Clear(); _jobWhitelists.Clear(); - _roleBans.Clear(); + _jobBans.Clear(); + _antagBans.Clear(); } } private void RxRoleBans(MsgRoleBans message) { - _sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries."); + _sawmill.Debug($"Received roleban info containing {message.JobBans.Count + message.AntagBans.Count} entries."); - _roleBans.Clear(); - _roleBans.AddRange(message.Bans); + _jobBans.Clear(); + _jobBans.AddRange(message.JobBans); + _antagBans.Clear(); + _antagBans.AddRange(message.AntagBans); Updated?.Invoke(); } @@ -95,7 +99,7 @@ public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotN { reason = null; - if (_roleBans.Contains($"Job:{job.ID}")) + if (_jobBans.Contains(new ProtoId(job.ID))) { reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban")); return false; diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs index 9e57cd4b0e6..5f77af1b104 100644 --- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs +++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs @@ -32,9 +32,9 @@ public async Task PardonTest() // 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 @@ public async Task PardonTest() // 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 @@ public async Task PardonTest() // 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 @@ public async Task PardonTest() 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 @@ public async Task PardonTest() 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 @@ public async Task PardonTest() Assert.Multiple(async () => { // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); // Reconnect client. Slightly faster than dirtying the pair. diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs new file mode 100644 index 00000000000..62dde10b99c --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs @@ -0,0 +1,2125 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20260120200503_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("boolean") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("boolean") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ipintel_cache_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("real") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs new file mode 100644 index 00000000000..64a20c2933e --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs @@ -0,0 +1,535 @@ +using System; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + type = table.Column(type: "smallint", nullable: false), + playtime_at_note = table.Column(type: "interval", nullable: false), + ban_time = table.Column(type: "timestamp with time zone", nullable: false), + expiration_time = table.Column(type: "timestamp with time zone", nullable: true), + reason = table.Column(type: "text", nullable: false), + severity = table.Column(type: "integer", nullable: false), + banning_admin = table.Column(type: "uuid", nullable: true), + last_edited_by_id = table.Column(type: "uuid", nullable: true), + last_edited_at = table.Column(type: "timestamp with time zone", nullable: true), + exempt_flags = table.Column(type: "integer", nullable: false), + auto_delete = table.Column(type: "boolean", nullable: false), + hidden = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + address = table.Column(type: "inet", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.CheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + hwid = table.Column(type: "bytea", nullable: false), + hwid_type = table.Column(type: "integer", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "uuid", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_type = table.Column(type: "text", nullable: false), + role_id = table.Column(type: "text", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + round_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + unbanning_admin = table.Column(type: "uuid", nullable: true), + unban_time = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.Sql(""" + CREATE INDEX "IX_ban_address_address" + ON ban_address + USING gist + (address inet_ops) + INCLUDE (ban_id); + + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql($""" + -- REMOVE: + -- TRUNCATE ban RESTART IDENTITY CASCADE; + + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, {(int)BanType.Server}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Update ID sequence to be after newly inserted IDs. + SELECT setval('ban_ban_id_seq', (SELECT MAX(ban_id) FROM ban)); + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_merge_map; + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_id_map; + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND date_trunc('second', main.ban_time, 'utc') = date_trunc('second', sub.ban_time, 'utc') + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR date_trunc('minute', main.expiration_time, 'utc') = date_trunc('minute', sub.expiration_time, 'utc') + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT INTO + _role_ban_import_id_map + SELECT + DISTINCT ON (merge_id) + nextval('ban_ban_id_seq'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, {(int)BanType.Role}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + split_part(role_id, ':', 1), split_part(role_id, ':', 2), im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + + migrationBuilder.Sql($""" + CREATE OR REPLACE FUNCTION send_server_ban_notification() + RETURNS trigger AS $$ + BEGIN + PERFORM pg_notify( + 'ban_notification', + json_build_object('ban_id', NEW.ban_id)::text + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER notify_on_server_ban_insert + AFTER INSERT ON ban + FOR EACH ROW + WHEN (NEW.type = {(int)BanType.Server}) + EXECUTE FUNCTION send_server_ban_notification(); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index ab476a51fc7..68e304f640e 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using System.Net; using System.Text.Json; using Content.Server.Database; @@ -20,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -518,6 +519,221 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -899,6 +1115,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("admin_ooc_color"); + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + b.Property("SelectedCharacterSlot") .HasColumnType("integer") .HasColumnName("selected_character_slot"); @@ -1197,137 +1418,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { - b.Property("Id") + b.Property("UserId") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("boolean") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") .HasColumnType("uuid") - .HasColumnName("banning_admin"); + .HasColumnName("user_id"); - b.Property("ExemptFlags") + b.Property("Flags") .HasColumnType("integer") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); + .HasColumnName("flags"); - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("RoundId") + b.Property("BanId") .HasColumnType("integer") - .HasColumnName("round_id"); + .HasColumnName("ban_id"); - b.Property("Severity") + b.Property("ConnectionId") .HasColumnType("integer") - .HasColumnName("severity"); + .HasColumnName("connection_id"); b.HasKey("Id") - .HasName("PK_server_ban"); + .HasName("PK_server_ban_hit"); - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); - - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => - { - b.Property("UserId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.Property("Flags") - .HasColumnType("integer") - .HasColumnName("flags"); - - b.HasKey("UserId") - .HasName("PK_server_ban_exemption"); - - b.ToTable("server_ban_exemption", null, t => - { - t.HasCheckConstraint("FlagsNotZero", "flags != 0"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_ban_hit_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BanId") - .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("ConnectionId") - .HasColumnType("integer") - .HasColumnName("connection_id"); - - b.HasKey("Id") - .HasName("PK_server_ban_hit"); - - b.HasIndex("BanId") - .HasDatabaseName("IX_server_ban_hit_ban_id"); + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); b.HasIndex("ConnectionId") .HasDatabaseName("IX_server_ban_hit_connection_id"); @@ -1335,123 +1467,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server_ban_hit", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); + .HasColumnName("trait_id"); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("uuid") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); - b.Property("RoleId") + b.Property("TraitName") .IsRequired() .HasColumnType("text") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("integer") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("integer") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_role_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_role_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_role_ban_round_id"); - - b.ToTable("server_role_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); - - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("role_unban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BanId") - .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("unban_time"); - - b.Property("UnbanningAdmin") - .HasColumnType("uuid") - .HasColumnName("unbanning_admin"); + .HasColumnName("trait_name"); b.HasKey("Id") - .HasName("PK_server_role_unban"); + .HasName("PK_trait"); - b.HasIndex("BanId") + b.HasIndex("ProfileId", "TraitName") .IsUnique(); - b.ToTable("server_role_unban", (string)null); + b.ToTable("trait", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1473,39 +1516,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("unbanning_admin"); b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_unban"); b.HasIndex("BanId") .IsUnique(); - b.ToTable("server_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("trait_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ProfileId") - .HasColumnType("integer") - .HasColumnName("profile_id"); - - b.Property("TraitName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("trait_name"); - - b.HasKey("Id") - .HasName("PK_trait"); - - b.HasIndex("ProfileId", "TraitName") - .IsUnique(); - - b.ToTable("trait", (string)null); + b.ToTable("unban", (string)null); }); modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => @@ -1792,6 +1808,123 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -1982,70 +2115,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -2059,98 +2136,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + .HasConstraintName("FK_trait_profile_profile_id"); - b.Navigation("Ban"); + b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .HasForeignKey("Content.Server.Database.Unban", "BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); + .HasConstraintName("FK_unban_ban_ban_id"); b.Navigation("Ban"); }); - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.HasOne("Content.Server.Database.Profile", "Profile") - .WithMany("Traits") - .HasForeignKey("ProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_trait_profile_profile_id"); - - b.Navigation("Profile"); - }); - modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -2185,6 +2194,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -2221,10 +2247,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2275,18 +2297,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs new file mode 100644 index 00000000000..804e3aae272 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs @@ -0,0 +1,2044 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20260120200455_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("INTEGER") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("INTEGER") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ipintel_cache_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("REAL") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection("ConstructionFavorites") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs new file mode 100644 index 00000000000..f813d109453 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs @@ -0,0 +1,498 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + type = table.Column(type: "INTEGER", nullable: false), + playtime_at_note = table.Column(type: "TEXT", nullable: false), + ban_time = table.Column(type: "TEXT", nullable: false), + expiration_time = table.Column(type: "TEXT", nullable: true), + reason = table.Column(type: "TEXT", nullable: false), + severity = table.Column(type: "INTEGER", nullable: false), + banning_admin = table.Column(type: "TEXT", nullable: true), + last_edited_by_id = table.Column(type: "TEXT", nullable: true), + last_edited_at = table.Column(type: "TEXT", nullable: true), + exempt_flags = table.Column(type: "INTEGER", nullable: false), + auto_delete = table.Column(type: "INTEGER", nullable: false), + hidden = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + address = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + hwid = table.Column(type: "BLOB", nullable: false), + hwid_type = table.Column(type: "INTEGER", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + user_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + role_type = table.Column(type: "TEXT", nullable: false), + role_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + round_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + unbanning_admin = table.Column(type: "TEXT", nullable: true), + unban_time = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql(""" + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, 0, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE); + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE); + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND main.ban_time = sub.ban_time + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR main.expiration_time = sub.expiration_time + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT OR IGNORE INTO + _role_ban_import_id_map + SELECT + merge_id + (SELECT seq FROM sqlite_sequence WHERE name = 'ban'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, 1, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + substr(role_id, 1, instr(role_id, ':')-1), + substr(role_id, instr(role_id, ':')+1), + im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 39d36d6fa2c..11f4f9fec23 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteServerDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); modelBuilder.Entity("Content.Server.Database.Admin", b => { @@ -489,6 +489,207 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -1133,91 +1334,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("INTEGER") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExemptFlags") - .HasColumnType("INTEGER") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { b.Property("UserId") @@ -1265,117 +1381,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server_ban_hit", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); + .HasColumnName("trait_id"); - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); - b.Property("BanTime") + b.Property("TraitName") + .IsRequired() .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); + .HasColumnName("trait_name"); 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 => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("role_unban_id"); - - b.Property("BanId") - .HasColumnType("INTEGER") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("TEXT") - .HasColumnName("unban_time"); - - b.Property("UnbanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("unbanning_admin"); - - b.HasKey("Id") - .HasName("PK_server_role_unban"); + .HasName("PK_trait"); - b.HasIndex("BanId") + b.HasIndex("ProfileId", "TraitName") .IsUnique(); - b.ToTable("server_role_unban", (string)null); + b.ToTable("trait", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1395,37 +1426,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("unbanning_admin"); b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_unban"); b.HasIndex("BanId") .IsUnique(); - b.ToTable("server_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("trait_id"); - - b.Property("ProfileId") - .HasColumnType("INTEGER") - .HasColumnName("profile_id"); - - b.Property("TraitName") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("trait_name"); - - b.HasKey("Id") - .HasName("PK_trait"); - - b.HasIndex("ProfileId", "TraitName") - .IsUnique(); - - b.ToTable("trait", (string)null); + b.ToTable("unban", (string)null); }); modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => @@ -1710,6 +1716,123 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -1900,70 +2023,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -1977,98 +2044,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + .HasConstraintName("FK_trait_profile_profile_id"); - b.Navigation("Ban"); + b.Navigation("Profile"); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .HasForeignKey("Content.Server.Database.Unban", "BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); + .HasConstraintName("FK_unban_ban_ban_id"); b.Navigation("Ban"); }); - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.HasOne("Content.Server.Database.Profile", "Profile") - .WithMany("Traits") - .HasForeignKey("ProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_trait_profile_profile_id"); - - b.Navigation("Profile"); - }); - modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -2103,6 +2102,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -2139,10 +2155,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2193,18 +2205,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Model.Ban.cs b/Content.Server.Database/Model.Ban.cs new file mode 100644 index 00000000000..7d1ee2ab1a1 --- /dev/null +++ b/Content.Server.Database/Model.Ban.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore; +using NpgsqlTypes; + +// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength + +namespace Content.Server.Database; + +// +// Contains model definitions primarily related to bans. +// + +internal static class ModelBan +{ + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(b => b.CreatedBy) + .WithMany(pl => pl.AdminServerBansCreated) + .HasForeignKey(b => b.BanningAdmin) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasOne(b => b.LastEditedBy) + .WithMany(pl => pl.AdminServerBansLastEdited) + .HasForeignKey(b => b.LastEditedById) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.UserId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .OwnsOne(bp => bp.HWId) + .Property(hwid => hwid.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoundId, bp.BanId }) + .IsUnique(); + + // Following indices have to be made manually by migration, due to limitations in EF Core: + // https://github.com/dotnet/efcore/issues/11336 + // https://github.com/npgsql/efcore.pg/issues/2567 + // modelBuilder.Entity() + // .HasIndex(bp => new { bp.Address, bp.BanId }) + // .IsUnique(); + // modelBuilder.Entity() + // .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid }) + // .IsUnique(); + // (postgres only) + // modelBuilder.Entity() + // .HasIndex(ba => ba.Address) + // .IncludeProperties(ba => ba.BanId) + // .IsUnique() + // .HasMethod("gist") + // .HasOperators("inet_ops"); + + modelBuilder.Entity() + .ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0")); + } +} + +/// +/// Specifies a ban of some kind. +/// +/// +/// +/// Bans come in two types: and , +/// distinguished with . +/// +/// +/// Bans have one or more "matching data", these being , , +/// and entities. If a player's connection info matches any of these, +/// the ban's effects will apply to that player. +/// +/// +/// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually +/// ("unbanned") by an admin, which is stored as an entity existing for this ban. +/// +/// +public sealed class Ban +{ + public int Id { get; set; } + + /// + /// Whether this is a role or server ban. + /// + public required BanType Type { get; set; } + + public TimeSpan PlaytimeAtNote { get; set; } + + /// + /// The time when the ban was applied by an administrator. + /// + public DateTime BanTime { get; set; } + + /// + /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. + /// + public DateTime? ExpirationTime { get; set; } + + /// + /// The administrator-stated reason for applying the ban. + /// + public string Reason { get; set; } = null!; + + /// + /// The severity of the incident + /// + public NoteSeverity Severity { get; set; } + + /// + /// User ID of the admin that initially applied the ban. + /// + [ForeignKey(nameof(CreatedBy))] + public Guid? BanningAdmin { get; set; } + + public Player? CreatedBy { get; set; } + + /// + /// User ID of the admin that last edited the note + /// + [ForeignKey(nameof(LastEditedBy))] + public Guid? LastEditedById { get; set; } + + public Player? LastEditedBy { get; set; } + public DateTime? LastEditedAt { get; set; } + + /// + /// Optional flags that allow adding exemptions to the ban via . + /// + public ServerBanExemptFlags ExemptFlags { get; set; } + + /// + /// Whether this ban should be automatically deleted from the database when it expires. + /// + /// + /// This isn't done automatically by the game, + /// you will need to set up something like a cron job to clear this from your database, + /// using a command like this: + /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" + /// + public bool AutoDelete { get; set; } + + /// + /// Whether to display this ban in the admin remarks (notes) panel + /// + public bool Hidden { get; set; } + + /// + /// If present, an administrator has manually repealed this ban. + /// + public Unban? Unban { get; set; } + + public List? Rounds { get; set; } + public List? Players { get; set; } + public List? Addresses { get; set; } + public List? Hwids { get; set; } + public List? Roles { get; set; } + public List? BanHits { get; set; } +} + +/// +/// Base type for entities that specify ban matching data. +/// +public interface IBanSelector +{ + int BanId { get; } + Ban? Ban { get; } +} + +/// +/// Indicates that a ban was related to a round (e.g. placed on that round). +/// +public sealed class BanRound +{ + public int Id { get; set; } + + /// + /// The ID of the ban to which this round was relevant. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } + + /// + /// The ID of the round to which this ban was relevant to. + /// + [ForeignKey(nameof(Round))] + public int RoundId { get; set; } + + public Round? Round { get; set; } +} + +/// +/// Specifies a player that a matches. +/// +public sealed class BanPlayer : IBanSelector +{ + public int Id { get; set; } + + /// + /// The user ID of the banned player. + /// + public Guid UserId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies an IP address range that a matches. +/// +public sealed class BanAddress : IBanSelector +{ + public int Id { get; set; } + + /// + /// The address range being matched. + /// + public required NpgsqlInet Address { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies a HWID that a matches. +/// +public sealed class BanHwid : IBanSelector +{ + public int Id { get; set; } + + /// + /// The HWID being matched. + /// + public required TypedHwid HWId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// A single role banned among a greater role ban record. +/// +/// +/// s of type should have one or more s +/// to store which roles are actually banned. +/// It is invalid for bans to have entities. +/// +public sealed class BanRole +{ + public int Id { get; set; } + + /// + /// What type of role is being banned. For example Job or Antag. + /// + public required string RoleType { get; set; } + + /// + /// The ID of the role being banned. This is probably something like a prototype. + /// + public required string RoleId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// An explicit repeal of a by an administrator. +/// Having an entry for a ban neutralizes it. +/// +public sealed class Unban +{ + public int Id { get; set; } + + /// + /// The ID of ban that is being repealed. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + /// + /// The ban that is being repealed. + /// + public Ban? Ban { get; set; } + + /// + /// The admin that repealed the ban. + /// + public Guid? UnbanningAdmin { get; set; } + + /// + /// The time the ban was repealed. + /// + public DateTime UnbanTime { get; set; } +} diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 6806e8502bb..568ab8fc676 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -9,7 +9,6 @@ using System.Text.Json; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; -using NpgsqlTypes; namespace Content.Server.Database { @@ -33,13 +32,17 @@ protected ServerDbContext(DbContextOptions options) : base(options) public DbSet AdminLogPlayer { get; set; } = null!; public DbSet Whitelist { get; set; } = null!; public DbSet Blacklist { get; set; } = null!; - public DbSet Ban { get; set; } = default!; - public DbSet Unban { get; set; } = default!; + public DbSet Ban { get; set; } = default!; + public DbSet BanRound { get; set; } = default!; + public DbSet BanPlayer { get; set; } = default!; + public DbSet BanAddress { get; set; } = default!; + public DbSet BanHwid { get; set; } = default!; + public DbSet BanRole { get; set; } = default!; + public DbSet Unban { get; set; } = default!; public DbSet BanExemption { get; set; } = default!; public DbSet ConnectionLog { get; set; } = default!; public DbSet ServerBanHit { get; set; } = default!; - public DbSet RoleBan { get; set; } = default!; - public DbSet RoleUnban { get; set; } = default!; + public DbSet PlayTime { get; set; } = default!; public DbSet UploadedResourceLog { get; set; } = default!; public DbSet AdminNotes { get; set; } = null!; @@ -179,43 +182,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId}); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - // Ban exemption can't have flags 0 since that wouldn't exempt anything. // The row should be removed if setting to 0. modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("FlagsNotZero", "flags != 0")); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - modelBuilder.Entity() .HasIndex(p => p.UserId) .IsUnique(); @@ -330,34 +301,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen")); - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerRoleBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerRoleBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - modelBuilder.Entity() .HasOne(w => w.Player) .WithMany(p => p.JobWhitelists) @@ -376,26 +319,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() .OwnsOne(p => p.HWId) .Property(p => p.Hwid) @@ -405,6 +328,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .OwnsOne(p => p.HWId) .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); + + ModelBan.OnModelCreating(modelBuilder); } public virtual IQueryable SearchLogs(IQueryable query, string searchText) @@ -693,10 +618,8 @@ public class Player public List AdminMessagesCreated { get; set; } = null!; public List AdminMessagesLastEdited { get; set; } = null!; public List AdminMessagesDeleted { get; set; } = null!; - public List AdminServerBansCreated { get; set; } = null!; - public List AdminServerBansLastEdited { get; set; } = null!; - public List AdminServerRoleBansCreated { get; set; } = null!; - public List AdminServerRoleBansLastEdited { get; set; } = null!; + public List AdminServerBansCreated { get; set; } = null!; + public List AdminServerBansLastEdited { get; set; } = null!; public List JobWhitelists { get; set; } = null!; } @@ -826,30 +749,6 @@ public class AdminLogPlayer [ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!; } - // Used by SS14.Admin - public interface IBanCommon where TUnban : IUnbanCommon - { - int Id { get; set; } - Guid? PlayerUserId { get; set; } - NpgsqlInet? Address { get; set; } - TypedHwid? HWId { get; set; } - DateTime BanTime { get; set; } - DateTime? ExpirationTime { get; set; } - string Reason { get; set; } - NoteSeverity Severity { get; set; } - Guid? BanningAdmin { get; set; } - TUnban? Unban { get; set; } - } - - // Used by SS14.Admin - public interface IUnbanCommon - { - int Id { get; set; } - int BanId { get; set; } - Guid? UnbanningAdmin { get; set; } - DateTime UnbanTime { get; set; } - } - /// /// Flags for use with . /// @@ -887,138 +786,6 @@ public enum ServerBanExemptFlags // @formatter:on } - /// - /// A ban from playing on the server. - /// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server. - /// - /// - /// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing). - /// - [Table("server_ban"), Index(nameof(PlayerUserId))] - public class ServerBan : IBanCommon - { - public int Id { get; set; } - - [ForeignKey("Round")] - public int? RoundId { get; set; } - public Round? Round { get; set; } - - /// - /// The user ID of the banned player. - /// - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - - /// - /// CIDR IP address range of the ban. The whole range can match the ban. - /// - public NpgsqlInet? Address { get; set; } - - /// - /// Hardware ID of the banned player. - /// - public TypedHwid? HWId { get; set; } - - /// - /// The time when the ban was applied by an administrator. - /// - public DateTime BanTime { get; set; } - - /// - /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. - /// - public DateTime? ExpirationTime { get; set; } - - /// - /// The administrator-stated reason for applying the ban. - /// - public string Reason { get; set; } = null!; - - /// - /// The severity of the incident - /// - public NoteSeverity Severity { get; set; } - - /// - /// User ID of the admin that applied the ban. - /// - [ForeignKey("CreatedBy")] - public Guid? BanningAdmin { get; set; } - - public Player? CreatedBy { get; set; } - - /// - /// User ID of the admin that last edited the note - /// - [ForeignKey("LastEditedBy")] - public Guid? LastEditedById { get; set; } - - public Player? LastEditedBy { get; set; } - - /// - /// When the ban was last edited - /// - public DateTime? LastEditedAt { get; set; } - - /// - /// Optional flags that allow adding exemptions to the ban via . - /// - public ServerBanExemptFlags ExemptFlags { get; set; } - - /// - /// If present, an administrator has manually repealed this ban. - /// - public ServerUnban? Unban { get; set; } - - /// - /// Whether this ban should be automatically deleted from the database when it expires. - /// - /// - /// This isn't done automatically by the game, - /// you will need to set up something like a cron job to clear this from your database, - /// using a command like this: - /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" - /// - public bool AutoDelete { get; set; } - - /// - /// Whether to display this ban in the admin remarks (notes) panel - /// - public bool Hidden { get; set; } - - public List BanHits { get; set; } = null!; - } - - /// - /// An explicit repeal of a by an administrator. - /// Having an entry for a ban neutralizes it. - /// - [Table("server_unban")] - public class ServerUnban : IUnbanCommon - { - [Column("unban_id")] public int Id { get; set; } - - /// - /// The ID of ban that is being repealed. - /// - public int BanId { get; set; } - - /// - /// The ban that is being repealed. - /// - public ServerBan Ban { get; set; } = null!; - - /// - /// The admin that repealed the ban. - /// - public Guid? UnbanningAdmin { get; set; } - - /// - /// The time the ban repealed. - /// - public DateTime UnbanTime { get; set; } - } - /// /// An exemption for a specific user to a certain type of . /// @@ -1039,7 +806,7 @@ public sealed class ServerBanExemption /// /// The ban flags to exempt this player from. - /// If any bit overlaps , the ban is ignored. + /// If any bit overlaps , the ban is ignored. /// public ServerBanExemptFlags Flags { get; set; } } @@ -1103,54 +870,10 @@ public class ServerBanHit public int BanId { get; set; } public int ConnectionId { get; set; } - public ServerBan Ban { get; set; } = null!; + public Ban Ban { get; set; } = null!; public ConnectionLog Connection { get; set; } = null!; } - [Table("server_role_ban"), Index(nameof(PlayerUserId))] - public sealed class ServerRoleBan : IBanCommon - { - public int Id { get; set; } - public int? RoundId { get; set; } - public Round? Round { get; set; } - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - public NpgsqlInet? Address { get; set; } - public TypedHwid? HWId { get; set; } - - public DateTime BanTime { get; set; } - - public DateTime? ExpirationTime { get; set; } - - public string Reason { get; set; } = null!; - - public NoteSeverity Severity { get; set; } - [ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; } - public Player? CreatedBy { get; set; } - - [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; } - public Player? LastEditedBy { get; set; } - public DateTime? LastEditedAt { get; set; } - - public ServerRoleUnban? Unban { get; set; } - public bool Hidden { get; set; } - - public string RoleId { get; set; } = null!; - } - - [Table("server_role_unban")] - public sealed class ServerRoleUnban : IUnbanCommon - { - [Column("role_unban_id")] public int Id { get; set; } - - public int BanId { get; set; } - public ServerRoleBan Ban { get; set; } = null!; - - public Guid? UnbanningAdmin { get; set; } - - public DateTime UnbanTime { get; set; } - } - [Table("play_time")] public sealed class PlayTime { @@ -1350,31 +1073,31 @@ public sealed class BanTemplate /// /// The reason for the ban. /// - /// + /// public string Reason { get; set; } = ""; /// /// Exemptions granted to the ban. /// - /// + /// public ServerBanExemptFlags ExemptFlags { get; set; } /// /// Severity of the ban /// - /// + /// public NoteSeverity Severity { get; set; } /// /// Ban will be automatically deleted once expired. /// - /// + /// public bool AutoDelete { get; set; } /// /// Ban is not visible to players in the remarks menu. /// - /// + /// public bool Hidden { get; set; } } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index 3de14223b3b..4d81c345e2c 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -40,10 +40,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // ReSharper disable StringLiteralTypo // Enforce that an address cannot be IPv6-mapped IPv4. // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); - - modelBuilder.Entity().ToTable( t => + modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); modelBuilder.Entity().ToTable(t => diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index d3412fa6586..aafe8202d40 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -56,13 +56,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); modelBuilder - .Entity() - .Property(e => e.Address) - .HasColumnType("TEXT") - .HasConversion(ipMaskConverter); - - modelBuilder - .Entity() + .Entity() .Property(e => e.Address) .HasColumnType("TEXT") .HasConversion(ipMaskConverter); diff --git a/Content.Server/Administration/BanList/BanListEui.cs b/Content.Server/Administration/BanList/BanListEui.cs index 2ca126bf164..549a14f6733 100644 --- a/Content.Server/Administration/BanList/BanListEui.cs +++ b/Content.Server/Administration/BanList/BanListEui.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; using Content.Server.Administration.Managers; using Content.Server.Database; using Content.Server.EUI; using Content.Shared.Administration; using Content.Shared.Administration.BanList; +using Content.Shared.Database; using Content.Shared.Eui; using Robust.Shared.Network; @@ -22,8 +25,8 @@ public BanListEui() private Guid BanListPlayer { get; set; } private string BanListPlayerName { get; set; } = string.Empty; - private List Bans { get; } = new(); - private List RoleBans { get; } = new(); + private List Bans { get; } = new(); + private List RoleBans { get; } = new(); public override void Opened() { @@ -54,74 +57,38 @@ private void OnPermsChanged(AdminPermsChangedEventArgs args) private async Task LoadBans(NetUserId userId) { - foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null)) - { - SharedServerUnban? unban = null; - if (ban.Unban is { } unbanDef) - { - var unbanningAdmin = unbanDef.UnbanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); - } - - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; - - if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) - { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); - } - - Bans.Add(new SharedServerBan( - ban.Id, - ban.UserId, - ip, - hwid, - ban.BanTime.UtcDateTime, - ban.ExpirationTime?.UtcDateTime, - ban.Reason, - ban.BanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, - unban - )); - } + await LoadBansCore(userId, BanType.Server, Bans); + await LoadBansCore(userId, BanType.Role, RoleBans); } - private async Task LoadRoleBans(NetUserId userId) + private async Task LoadBansCore(NetUserId userId, BanType banType, List list) { - foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null)) + foreach (var ban in await _db.GetBansAsync(null, userId, null, null, type: banType)) { - SharedServerUnban? unban = null; + SharedUnban? unban = null; if (ban.Unban is { } unbanDef) { var unbanningAdmin = unbanDef.UnbanningAdmin == null ? null : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); + unban = new SharedUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); } - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; + ImmutableArray<(string, int cidrMask)> ips = [("*Hidden*", 0)]; + ImmutableArray hwids = ["*Hidden*"]; if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); + ips = [..ban.Addresses.Select(a => (a.address.ToString(), a.cidrMask))]; + hwids = [..ban.HWIds.Select(h => h.ToString())]; } - RoleBans.Add(new SharedServerRoleBan( + + list.Add(new SharedBan( ban.Id, - ban.UserId, - ip, - hwid, + ban.Type, + ban.UserIds, + ips, + hwids, ban.BanTime.UtcDateTime, ban.ExpirationTime?.UtcDateTime, ban.Reason, @@ -129,7 +96,7 @@ private async Task LoadRoleBans(NetUserId userId) ? null : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, unban, - ban.Role + ban.Roles )); } } @@ -144,7 +111,6 @@ private async Task LoadFromDb() string.Empty; await LoadBans(userId); - await LoadRoleBans(userId); StateDirty(); } diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs index 3eedad3ed50..087a26b36d2 100644 --- a/Content.Server/Administration/BanPanelEui.cs +++ b/Content.Server/Administration/BanPanelEui.cs @@ -6,8 +6,10 @@ using Content.Server.EUI; using Content.Shared.Administration; using Content.Shared.Database; +using Content.Shared.Roles; using Content.Shared.Eui; using Robust.Shared.Network; +using Robust.Shared.Prototypes; namespace Content.Server.Administration; @@ -19,6 +21,7 @@ public sealed class BanPanelEui : BaseEui [Dependency] private readonly IPlayerLocator _playerLocator = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IAdminManager _admins = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; private readonly ISawmill _sawmill; @@ -26,8 +29,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() { @@ -70,6 +73,15 @@ private async void BanPlayer(string? target, string? ipAddressString, bool useLa return; } + var isRoleBan = roles?.Count > 0; + + CreateBanInfo banInfo = isRoleBan ? new CreateRoleBanInfo(reason) : new CreateServerBanInfo(reason); + + banInfo.WithBanningAdmin(Player.UserId); + banInfo.WithSeverity(severity); + if (minutes > 0) + banInfo.WithMinutes(minutes); + (IPAddress, int)? addressRange = null; if (ipAddressString is not null) { @@ -92,6 +104,7 @@ private async void BanPlayer(string? target, string? ipAddressString, bool useLa } var targetUid = target is not null ? PlayerId : null; + var targetName = target is not null ? PlayerName : string.Empty; addressRange = useLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange; var targetHWid = useLastHwid ? LastHwid : hwid; if (target != null && target != PlayerName || Guid.TryParse(target, out var parsed) && parsed != PlayerId) @@ -103,6 +116,7 @@ private async void BanPlayer(string? target, string? ipAddressString, bool useLa return; } targetUid = located.UserId; + targetName = located.Username; var targetAddress = located.LastAddress; if (useLastIp && targetAddress != null) { @@ -116,20 +130,36 @@ private async void BanPlayer(string? target, string? ipAddressString, bool useLa targetHWid = useLastHwid ? located.LastHWId : hwid; } - if (roles?.Count > 0) + if (addressRange != null) + banInfo.AddAddressRange(addressRange.Value); + + if (targetUid != null) + banInfo.AddUser(targetUid.Value, targetName); + + banInfo.AddHWId(targetHWid); + + if (isRoleBan) { - var now = DateTimeOffset.UtcNow; - foreach (var role in roles) + var roleBanInfo = (CreateRoleBanInfo) banInfo; + foreach (var role in roles!) { - _banManager.CreateRoleBan(targetUid, target, Player.UserId, addressRange, targetHWid, role, minutes, severity, reason, now); + if (_prototypes.HasIndex(role)) + roleBanInfo.AddJob(new ProtoId(role)); + else if (_prototypes.HasIndex(role)) + roleBanInfo.AddAntag(new ProtoId(role)); + else + { + _chat.DispatchServerMessage(Player, Loc.GetString("cmd-roleban-job-parse", ("job", role))); + return; + } } + _banManager.CreateRoleBan(roleBanInfo); Close(); return; } - if (erase && - targetUid != null) + if (erase && targetUid is not null) { try { @@ -142,7 +172,7 @@ private async void BanPlayer(string? target, string? ipAddressString, bool useLa } } - _banManager.CreateServerBan(targetUid, target, Player.UserId, addressRange, targetHWid, minutes, severity, reason); + _banManager.CreateServerBan((CreateServerBanInfo) banInfo); Close(); } diff --git a/Content.Server/Administration/Commands/BanCommand.cs b/Content.Server/Administration/Commands/BanCommand.cs index f76cfded814..a6d3b106462 100644 --- a/Content.Server/Administration/Commands/BanCommand.cs +++ b/Content.Server/Administration/Commands/BanCommand.cs @@ -90,7 +90,15 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] var targetUid = located.UserId; var targetHWid = located.LastHWId; - _bans.CreateServerBan(targetUid, target, player?.UserId, null, targetHWid, minutes, severity, reason); + var banInfo = new CreateServerBanInfo(reason); + banInfo.WithBanningAdmin(player?.UserId); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.WithSeverity(severity); + + _bans.CreateServerBan(banInfo); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/BanListCommand.cs b/Content.Server/Administration/Commands/BanListCommand.cs index 2f7093ae1d8..98e57028243 100644 --- a/Content.Server/Administration/Commands/BanListCommand.cs +++ b/Content.Server/Administration/Commands/BanListCommand.cs @@ -38,7 +38,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); + var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); if (bans.Count == 0) { diff --git a/Content.Server/Administration/Commands/DepartmentBanCommand.cs b/Content.Server/Administration/Commands/DepartmentBanCommand.cs index 15f9859ca10..40faeb74048 100644 --- a/Content.Server/Administration/Commands/DepartmentBanCommand.cs +++ b/Content.Server/Administration/Commands/DepartmentBanCommand.cs @@ -96,13 +96,20 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) var targetUid = located.UserId; var targetHWid = located.LastHWId; - // If you are trying to remove the following variable, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Without it the note list will get needlessly cluttered. - var now = DateTimeOffset.UtcNow; + var banInfo = new CreateRoleBanInfo(reason); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.AddUser(targetUid, located.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(targetHWid); + banInfo.WithSeverity(severity); + foreach (var job in departmentProto.Roles) { - _banManager.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, now); + banInfo.AddJob(job); } + + _banManager.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs index cbe1e89e19c..9f022eb95e2 100644 --- a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs +++ b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs @@ -3,6 +3,7 @@ using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; +using Robust.Shared.Network; namespace Content.Server.Administration.Commands; @@ -44,7 +45,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] return; } - await IoCManager.Resolve().OpenEui(player, notedPlayer); + await IoCManager.Resolve().OpenEui(player, new NetUserId(notedPlayer)); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/PardonCommand.cs b/Content.Server/Administration/Commands/PardonCommand.cs index 9cbaaece310..b1f525dcf49 100644 --- a/Content.Server/Administration/Commands/PardonCommand.cs +++ b/Content.Server/Administration/Commands/PardonCommand.cs @@ -29,7 +29,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) return; } - var ban = await dbMan.GetServerBanAsync(banId); + var ban = await dbMan.GetBanAsync(banId); if (ban == null) { @@ -52,7 +52,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) return; } - await dbMan.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now)); + await dbMan.AddUnbanAsync(new UnbanDef(banId, player?.UserId, DateTimeOffset.Now)); shell.WriteLine($"Pardoned ban with id {banId}"); } diff --git a/Content.Server/Administration/Commands/PrisonerCommands.cs b/Content.Server/Administration/Commands/PrisonerCommands.cs index 4afb1390551..ccc1e778a65 100644 --- a/Content.Server/Administration/Commands/PrisonerCommands.cs +++ b/Content.Server/Administration/Commands/PrisonerCommands.cs @@ -68,10 +68,16 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] Logger.WarningS("admin.role_ban", "Imprisonment roleban severity could not be parsed from config! Defaulting to medium."); severity = NoteSeverity.Medium; } - var now = DateTimeOffset.UtcNow; // this groups bans together foreach (var proto in _prototypes.EnumeratePrototypes().Where(value => value.ID != "Prisoner")) { - _bans.CreateRoleBan(guid, data.Username, shell.Player?.UserId, null, data.LastHWId, proto.ID, 0, severity, "cmd-permabrig-ban-description", now); + var banInfo = new CreateRoleBanInfo("cmd-permabrig-ban-description"); + banInfo.AddUser(guid, data.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(data.LastHWId); + banInfo.WithSeverity(severity); + banInfo.AddJob(new ProtoId(proto.ID)); + + _bans.CreateRoleBan(banInfo); } _jobWhitelist.AddWhitelist(guid, prisonerJob); diff --git a/Content.Server/Administration/Commands/RoleBanCommand.cs b/Content.Server/Administration/Commands/RoleBanCommand.cs index 7058803d2f0..6803d424e48 100644 --- a/Content.Server/Administration/Commands/RoleBanCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanCommand.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.Managers; +using Content.Server.Administration.Managers; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Database; @@ -99,7 +97,17 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) var targetUid = located.UserId; var targetHWid = located.LastHWId; - _bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, 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); + + banInfo.AddJob(new ProtoId(job)); + + _bans.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/RoleBanListCommand.cs b/Content.Server/Administration/Commands/RoleBanListCommand.cs index 8244ded3b20..4abd406cbc0 100644 --- a/Content.Server/Administration/Commands/RoleBanListCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanListCommand.cs @@ -1,10 +1,8 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.BanList; +using Content.Server.Administration.BanList; using Content.Server.EUI; using Content.Server.Database; using Content.Shared.Administration; -using Robust.Server.Player; +using Content.Shared.Database; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; @@ -48,7 +46,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) 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 async void Execute(IConsoleShell shell, string argStr, string[] args) foreach (var ban in bans) { - var msg = $"ID: {ban.Id}: Role: {ban.Role} Reason: {ban.Reason}"; + var msg = $"ID: {ban.Id}: Role(s): {string.Join(",", ban.Roles ?? [])} Reason: {ban.Reason}"; shell.WriteLine(msg); } return; diff --git a/Content.Server/Administration/Managers/BanManager.Notification.cs b/Content.Server/Administration/Managers/BanManager.Notification.cs index ff84887f00d..d627dc508f1 100644 --- a/Content.Server/Administration/Managers/BanManager.Notification.cs +++ b/Content.Server/Administration/Managers/BanManager.Notification.cs @@ -41,14 +41,8 @@ private bool OnDatabaseNotificationEarlyFilter() 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 @@ private sealed class BanNotificationData /// [JsonRequired, JsonPropertyName("ban_id")] public int BanId { get; init; } - - /// - /// The id of the server the ban was made on. - /// This is used to avoid double work checking the ban on the originating server. - /// - /// - /// This is optional in case the ban was made outside a server (SS14.Admin) - /// - [JsonPropertyName("server_id")] - public int? ServerId { get; init; } } } diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs index 5a00b781399..56f1e1f1460 100644 --- a/Content.Server/Administration/Managers/BanManager.cs +++ b/Content.Server/Administration/Managers/BanManager.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -21,6 +20,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Administration.Managers; @@ -43,9 +43,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit private ISawmill _sawmill = default!; public const string SawmillId = "admin.bans"; - public const string JobPrefix = "Job:"; + public const string DbTypeAntag = "Antag"; + public const string DbTypeJob = "Job"; - private readonly Dictionary> _cachedRoleBans = new(); + private readonly Dictionary> _cachedRoleBans = new(); // Cached ban exemption flags are used to handle private readonly Dictionary _cachedBanExemptions = new(); @@ -71,9 +72,15 @@ private async Task CachePlayerData(ICommonSession player, CancellationToken canc var netChannel = player.Channel; ImmutableArray? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId; var modernHwids = netChannel.UserData.ModernHWIds; - var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false); - - var userRoleBans = new List(); + var roleBans = await _db.GetBansAsync( + netChannel.RemoteEndPoint.Address, + player.UserId, + hwId, + modernHwids, + false, + type: BanType.Role); + + var userRoleBans = new List(); foreach (var ban in roleBans) { userRoleBans.Add(ban); @@ -91,30 +98,6 @@ private void ClearPlayerData(ICommonSession player) _cachedBanExemptions.Remove(player); } - private async Task AddRoleBan(ServerRoleBanDef banDef) - { - banDef = await _db.AddServerRoleBanAsync(banDef); - - if (banDef.UserId != null - && _playerManager.TryGetSessionById(banDef.UserId, out var player) - && _cachedRoleBans.TryGetValue(player, out var cachedBans)) - { - cachedBans.Add(banDef); - } - - return true; - } - - public HashSet? GetRoleBans(NetUserId playerUserId) - { - if (!_playerManager.TryGetSessionById(playerUserId, out var session)) - return null; - - return _cachedRoleBans.TryGetValue(session, out var roleBans) - ? roleBans.Select(banDef => banDef.Role).ToHashSet() - : null; - } - public void Restart() { // Clear out players that have disconnected. @@ -138,43 +121,37 @@ public void Restart() } #region Server Bans - public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason) + public async void CreateServerBan(CreateServerBanInfo banInfo) { - DateTimeOffset? expires = null; - if (minutes > 0) + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Server, null); + + await _db.AddBanAsync(banDef); + + if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules)) { - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + // Reset their last read rules. They probably need a refresher! + foreach (var (userId, _) in banInfo.Users) + { + await _db.SetLastReadRules(userId, null); + } } - _systems.TryGetEntitySystem(out var ticker); - int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; - var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; - - var banDef = new ServerBanDef( - null, - target, - addressRange, - hwid, - DateTimeOffset.Now, - expires, - roundId, - playtime, - reason, - severity, - banningAdmin, - null); - - await _db.AddServerBanAsync(banDef); - if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null) - await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher! - var adminName = banningAdmin == null + var adminName = banInfo.BanningAdmin == null ? Loc.GetString("system-user") - : (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); - var targetName = target is null ? "null" : $"{targetUsername} ({target})"; - var addressRangeString = addressRange != null - ? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}" - : "null"; - var hwidString = hwid?.ToString() ?? "null"; + : (await _db.GetPlayerRecordByUserId(banInfo.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); + + var targetName = banInfo.Users.Count == 0 + ? "null" + : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})")); + + var addressRangeString = banInfo.AddressRanges.Count == 0 + ? "null" + : string.Join(", ", banInfo.AddressRanges.Select(a => $"{a.Address}/{a.Mask}")); + + var hwidString = banInfo.HWIds.Count == 0 + ? "null" + : string.Join(", ", banInfo.HWIds); + var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}"; var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii"; @@ -182,12 +159,12 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net 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); @@ -195,7 +172,19 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net KickMatchingConnectedPlayers(banDef, "newly placed ban"); } - private void KickMatchingConnectedPlayers(ServerBanDef def, string source) + private NoteSeverity GetSeverityForServerBan(CreateBanInfo banInfo, CVarDef defaultCVar) + { + if (banInfo.Severity != null) + return banInfo.Severity.Value; + + if (Enum.TryParse(_cfg.GetCVar(defaultCVar), true, out NoteSeverity parsedSeverity)) + return parsedSeverity; + + _sawmill.Error($"CVar {defaultCVar.Name} has invalid ban severity!"); + return NoteSeverity.None; + } + + private void KickMatchingConnectedPlayers(BanDef def, string source) { foreach (var player in _playerManager.Sessions) { @@ -207,7 +196,7 @@ private void KickMatchingConnectedPlayers(ServerBanDef def, string source) } } - private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) + private bool BanMatchesPlayer(ICommonSession player, BanDef ban) { var playerInfo = new BanMatcher.PlayerInfo { @@ -224,7 +213,7 @@ private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) 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); @@ -232,66 +221,156 @@ private void KickForBanDef(ICommonSession player, ServerBanDef def) #endregion - #region Job Bans - // If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset. - public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan) + #region Role Bans + + public async void CreateRoleBan(CreateRoleBanInfo banInfo) { - if (!_prototypeManager.TryIndex(role, out JobPrototype? _)) + ImmutableArray roleDefs = + [ + .. ToBanRoleDef(banInfo.JobPrototypes), + .. ToBanRoleDef(banInfo.AntagPrototypes), + ]; + + if (roleDefs.Length == 0) + throw new ArgumentException("Must specify at least one role to ban!"); + + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Role, roleDefs); + + await AddRoleBan(banDef); + + var length = expires == null + ? Loc.GetString("cmd-roleban-inf") + : Loc.GetString("cmd-roleban-until", ("expires", expires)); + + 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) { - throw new ArgumentException($"Invalid role '{role}'", nameof(role)); + if (_playerManager.TryGetSessionById(userId, out var session)) + SendRoleBans(session); } + } + + private async Task<(BanDef Ban, DateTimeOffset? Expires)> CreateBanDef( + CreateBanInfo banInfo, + BanType type, + ImmutableArray? roleBans) + { + if (banInfo.Users.Count == 0 && banInfo.HWIds.Count == 0 && banInfo.AddressRanges.Count == 0) + throw new ArgumentException("Must specify at least one user, HWID, or address range"); - role = string.Concat(JobPrefix, role); DateTimeOffset? expires = null; - if (minutes > 0) + if (banInfo.Duration is { } duration) + expires = DateTimeOffset.Now + duration; + + ImmutableArray roundIds; + if (banInfo.RoundIds.Count > 0) { - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + roundIds = [..banInfo.RoundIds]; + } + else if (_systems.TryGetEntitySystem(out var ticker) && ticker.RoundId != 0) + { + roundIds = [ticker.RoundId]; + } + else + { + roundIds = []; } - _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; - - 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, - role); + roles: roleBans), expires); + } + + private async Task GetPlayTime(CreateBanInfo banInfo) + { + var firstPlayer = banInfo.Users.FirstOrNull()?.UserId; + if (firstPlayer == null) + return TimeSpan.Zero; - if (!await AddRoleBan(banDef)) + return (await _db.GetPlayTimes(firstPlayer.Value)) + .Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall) + ?.TimeSpent ?? TimeSpan.Zero; + } + + private IEnumerable ToBanRoleDef(IEnumerable> protoIds) where T : class, IPrototype + { + return protoIds.Select(protoId => { - _chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role))); - return; - } + // TODO: I have no idea if this check is necessary. The previous code was a complete mess, + // so out of safety I'm leaving this in. + if (_prototypeManager.HasIndex(protoId) && _prototypeManager.HasIndex(protoId)) + { + throw new InvalidOperationException( + $"Creating role ban for {protoId}: cannot create role ban, role is both JobPrototype and AntagPrototype."); + } + + // Don't trust the input: make sure the role actually exists. + if (!_prototypeManager.HasIndex(protoId)) + throw new UnknownPrototypeException(protoId, typeof(T)); + + return new BanRoleDef(PrototypeKindToDbType(), protoId); + }); + } + + private static string PrototypeKindToDbType() where T : class, IPrototype + { + if (typeof(T) == typeof(JobPrototype)) + return DbTypeJob; - 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))); + if (typeof(T) == typeof(AntagPrototype)) + return DbTypeAntag; - if (target != null && _playerManager.TryGetSessionById(target.Value, out var session)) + throw new ArgumentException($"Unknown prototype kind for role bans: {typeof(T)}"); + } + + private async Task AddRoleBan(BanDef banDef) + { + banDef = await _db.AddBanAsync(banDef); + + foreach (var user in banDef.UserIds) { - SendRoleBans(session); + if (_playerManager.TryGetSessionById(user, out var player) + && _cachedRoleBans.TryGetValue(player, out var cachedBans)) + { + cachedBans.Add(banDef); + } } } public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) { - var ban = await _db.GetServerRoleBanAsync(banId); + var ban = await _db.GetBanAsync(banId); if (ban == null) { return $"No ban found with id {banId}"; } + if (ban.Type != BanType.Role) + throw new InvalidOperationException("Ban was not a role ban!"); + if (ban.Unban != null) { var response = new StringBuilder("This ban has already been pardoned"); @@ -305,40 +384,99 @@ public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da 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}"; } public HashSet>? GetJobBans(NetUserId playerUserId) + { + return GetRoleBans(playerUserId); + } + + public HashSet>? GetAntagBans(NetUserId playerUserId) + { + return GetRoleBans(playerUserId); + } + + private HashSet>? GetRoleBans(NetUserId playerUserId) where T : class, IPrototype { if (!_playerManager.TryGetSessionById(playerUserId, out var session)) return null; - if (!_cachedRoleBans.TryGetValue(session, out var roleBans)) + return GetRoleBans(session); + } + + private HashSet>? GetRoleBans(ICommonSession playerSession) where T : class, IPrototype + { + if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans)) return null; + var dbType = PrototypeKindToDbType(); + return roleBans - .Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal)) - .Select(ban => new ProtoId(ban.Role[JobPrefix.Length..])) + .SelectMany(ban => ban.Roles!.Value) + .Where(role => role.RoleType == dbType) + .Select(role => new ProtoId(role.RoleId)) .ToHashSet(); } - #endregion + + public HashSet? GetRoleBans(NetUserId playerUserId) + { + if (!_playerManager.TryGetSessionById(playerUserId, out var session)) + return null; + + return _cachedRoleBans.TryGetValue(session, out var roleBans) + ? roleBans.SelectMany(banDef => banDef.Roles ?? []).ToHashSet() + : null; + } + + public bool IsRoleBanned(ICommonSession player, List> jobs) + { + return IsRoleBanned(player, jobs); + } + + public bool IsRoleBanned(ICommonSession player, List> antags) + { + return IsRoleBanned(player, antags); + } + + private bool IsRoleBanned(ICommonSession player, List> roles) where T : class, IPrototype + { + var bans = GetRoleBans(player.UserId); + + if (bans is null || bans.Count == 0) + return false; + + var dbType = PrototypeKindToDbType(); + + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var role in roles) + { + if (bans.Contains(new BanRoleDef(dbType, role))) + return true; + } + + return false; + } public void SendRoleBans(ICommonSession pSession) { - var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List(); var bans = new MsgRoleBans() { - Bans = roleBans.Select(o => o.Role).ToList() + JobBans = (GetRoleBans(pSession) ?? []).ToList(), + AntagBans = (GetRoleBans(pSession) ?? []).ToList(), }; _sawmill.Debug($"Sent rolebans to {pSession.Name}"); @@ -353,48 +491,25 @@ public void PostInject() public async Task PardonRoleBanByDescription(string description, NetUserId? target, NetUserId? unbanningAdmin) // KS14 - used for imprisonment system - can go without stringent checking for errors { _playerManager.TryGetSessionById(target, out var session); - if (session == null) return "Error - cannot get target session."; + if (session == null) + return "Error - cannot get target session."; + _cachedRoleBans.TryGetValue(session, out var roleBans); if (roleBans != null) { // Iterate over role bans to unban those with the specific reason - foreach (var roleBan in roleBans.Where(rb => rb.Reason == description)) + foreach (var roleBan in roleBans.Where(rb => rb.Reason == description).ToArray()) { - if (roleBan.Unban == null) + if (roleBan.Unban == null && roleBan.Id is { } banId) { - int banId = roleBan.Id ?? 0; //evil hack - await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); + await _db.AddUnbanAsync(new UnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); } } roleBans.RemoveAll(roleBan => roleBan.Reason == description); SendRoleBans(session); } return $"Pardoned all bans with description {description}"; - /*var ban = await _db.GetServerRoleBanAsync(banId); - if (ban == null) - { - return $"No ban found with id {banId}"; - } - if (ban.Unban != null) - { - var response = new StringBuilder("This ban has already been pardoned"); - if (ban.Unban.UnbanningAdmin != null) - { - response.Append($" by {ban.Unban.UnbanningAdmin.Value}"); - } - response.Append($" in {ban.Unban.UnbanTime}."); - return response.ToString(); - } - var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List(); - await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); - if (ban.UserId is { } player - && _playerManager.TryGetSessionById(player, out var session) - && _cachedRoleBans.TryGetValue(session, out var roleBans)) - { - roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); - SendRoleBans(session); - } - return $"Pardoned ban with id {banId}";*/ - } + + #endregion } diff --git a/Content.Server/Administration/Managers/IBanManager.cs b/Content.Server/Administration/Managers/IBanManager.cs index 95962929570..73f148c3985 100644 --- a/Content.Server/Administration/Managers/IBanManager.cs +++ b/Content.Server/Administration/Managers/IBanManager.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Content.Shared.Database; using Content.Shared.Roles; @@ -14,6 +15,11 @@ public interface IBanManager public void Initialize(); public void Restart(); + /// + /// Create a server ban in the database, blocking connection for matching players. + /// + void CreateServerBan(CreateServerBanInfo banInfo); + /// /// Bans the specified target, address range and / or HWID. One of them must be non-null /// @@ -24,23 +30,78 @@ public interface IBanManager /// Number of minutes to ban for. 0 and null mean permanent /// Severity of the resulting ban note /// Reason for the ban - public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); - public HashSet? GetRoleBans(NetUserId playerUserId); + [Obsolete("Use CreateServerBan(CreateBanInfo) instead")] + public void CreateServerBan(NetUserId? target, + string? targetUsername, + NetUserId? banningAdmin, + (IPAddress, int)? addressRange, + ImmutableTypedHwid? hwid, + uint? minutes, + NoteSeverity severity, + string reason) + { + var info = new CreateServerBanInfo(reason); + if (target != null) + { + ArgumentNullException.ThrowIfNull(targetUsername); + info.AddUser(target.Value, targetUsername); + } + + if (addressRange != null) + info.AddAddressRange(addressRange.Value); + + if (hwid != null) + info.AddHWId(hwid); + + if (minutes > 0) + info.WithMinutes(minutes.Value); + + if (banningAdmin != null) + info.WithBanningAdmin(banningAdmin.Value); + + info.WithSeverity(severity); + + CreateServerBan(info); + } + + /// + /// Gets a list of prefixed prototype IDs with the player's role bans. + /// + public HashSet? GetRoleBans(NetUserId playerUserId); + + /// + /// Checks if the player is currently banned from any of the listed roles. + /// + /// The player. + /// A list of valid antag prototype IDs. + /// Returns True if an active role ban is found for this player for any of the listed roles. + public bool IsRoleBanned(ICommonSession player, List> antags); + + /// + /// Checks if the player is currently banned from any of the listed roles. + /// + /// The player. + /// A list of valid job prototype IDs. + /// Returns True if an active role ban is found for this player for any of the listed roles. + public bool IsRoleBanned(ICommonSession player, List> jobs); + + /// + /// Gets a list of prototype IDs with the player's job bans. + /// public HashSet>? GetJobBans(NetUserId playerUserId); /// - /// Creates a job ban for the specified target, username or GUID + /// Gets a list of prototype IDs with the player's antag bans. /// - /// Target user, username or GUID, null for none - /// Role to be banned from - /// Severity of the resulting ban note - /// Reason for the ban - /// Number of minutes to ban for. 0 and null mean permanent - /// Time when the ban was applied, used for grouping role bans - public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan); + public HashSet>? GetAntagBans(NetUserId playerUserId); + + /// + /// Creates a role ban, preventing matching players from playing said roles. + /// + public void CreateRoleBan(CreateRoleBanInfo banInfo); /// - /// Pardons a role ban for the specified target, username or GUID + /// Pardons a role ban by its ID. /// /// The id of the role ban to pardon. /// The admin, if any, that pardoned the role ban. @@ -61,3 +122,287 @@ public interface IBanManager /// Unbanning admin public Task PardonRoleBanByDescription(string description, NetUserId? target, NetUserId? unbanningAdmin); } + +/// +/// Base info to fill out in created ban records. +/// +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public abstract class CreateBanInfo +{ + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv4 = 32; + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv6 = 64; + + internal readonly HashSet<(NetUserId UserId, string UserName)> Users = []; + internal readonly HashSet<(IPAddress Address, int Mask)> AddressRanges = []; + internal readonly HashSet HWIds = []; + internal readonly HashSet RoundIds = []; + internal TimeSpan? Duration; + internal NoteSeverity? Severity; + internal string Reason; + internal NetUserId? BanningAdmin; + + protected CreateBanInfo(string reason) + { + Reason = reason; + } + + /// + /// Add a user to be matched by the ban. + /// + /// + /// Bans can target multiple users at once. + /// + /// The ID of the user. + /// The name of the user (used for logging purposes). + /// The current object, for easy chaining. + public CreateBanInfo AddUser(NetUserId userId, string username) + { + Users.Add((userId, username)); + return this; + } + + /// + /// Add an IP address to be matched by the ban. + /// + /// + /// Bans can target multiple addresses at once. + /// + /// + /// The IP address to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddress(IPAddress? address) + { + if (address == null) + return this; + + return AddAddressRange( + address, + address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange) + { + return AddAddressRange(addressRange.Address, addressRange.Mask); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange(IPAddress address, int mask) + { + AddressRanges.Add((address, mask)); + return this; + } + + /// + /// Add a hardware IP (HWID) to be matched by the ban. + /// + /// + /// Bans can target multiple HWIDs at once. + /// + /// + /// The HWID to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId) + { + if (hwId != null) + HWIds.Add(hwId); + + return this; + } + + /// + /// Add a relevant round ID to this ban. + /// + /// + /// + /// If not specified, the current round ID is used for the ban. + /// Therefore, the first call to this function will replace the round ID, + /// and further calls will add additional round IDs. + /// + /// + /// Bans can target multiple round IDs at once. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo AddRoundId(int roundId) + { + RoundIds.Add(roundId); + return this; + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(int minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithMinutes((uint)minutes); + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(uint minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithDuration(TimeSpan.FromMinutes(minutes)); + } + + /// + /// Set how long the ban will last. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive amount of time. + /// + public CreateBanInfo WithDuration(TimeSpan duration) + { + if (duration <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero."); + + Duration = duration; + return this; + } + + /// + /// Set the severity of the ban. + /// + /// + /// If no severity is specified, the default is specified through server configuration. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo WithSeverity(NoteSeverity severity) + { + Severity = severity; + return this; + } + + /// + /// Set the reason for the ban. + /// + /// + /// This replaces the value given via the object constructor. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithReason(string reason) + { + Reason = reason; + return this; + } + + /// + /// Specify the admin responsible for placing the ban. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin) + { + BanningAdmin = banningAdmin; + return this; + } +} + +/// +/// Stores info to create server ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateServerBanInfo : CreateBanInfo +{ + /// The reason for the server ban. + public CreateServerBanInfo(string reason) : base(reason) + { + } +} + +/// +/// Stores info to create role ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateRoleBanInfo : CreateBanInfo +{ + internal readonly HashSet> AntagPrototypes = []; + internal readonly HashSet> JobPrototypes = []; + + /// The reason for the role ban. + public CreateRoleBanInfo(string reason) : base(reason) + { + } + + /// + /// Add an antag role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddAntag(ProtoId protoId) + { + AntagPrototypes.Add(protoId); + return this; + } + + /// + /// Add a job role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddJob(ProtoId protoId) + { + JobPrototypes.Add(protoId); + return this; + } +} diff --git a/Content.Server/Administration/Notes/AdminNotesEui.cs b/Content.Server/Administration/Notes/AdminNotesEui.cs index d1297b251d7..5ecb9c774dc 100644 --- a/Content.Server/Administration/Notes/AdminNotesEui.cs +++ b/Content.Server/Administration/Notes/AdminNotesEui.cs @@ -22,7 +22,7 @@ public AdminNotesEui() 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 override async void HandleMessage(EuiMessageBase msg) } } - public async Task ChangeNotedPlayer(Guid notedPlayer) + public async Task ChangeNotedPlayer(NetUserId notedPlayer) { NotedPlayer = notedPlayer; await LoadFromDb(); @@ -120,7 +120,7 @@ public async Task ChangeNotedPlayer(Guid notedPlayer) 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 @@ private void NoteModified(SharedAdminNote note) private void NoteDeleted(SharedAdminNote note) { - if (note.Player != NotedPlayer) + if (!note.Players.Contains(NotedPlayer)) return; Notes.Remove((note.Id, note.NoteType)); diff --git a/Content.Server/Administration/Notes/AdminNotesExtensions.cs b/Content.Server/Administration/Notes/AdminNotesExtensions.cs index 349c7ff3bdf..e2ec62ed618 100644 --- a/Content.Server/Administration/Notes/AdminNotesExtensions.cs +++ b/Content.Server/Administration/Notes/AdminNotesExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Linq; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; @@ -11,7 +13,7 @@ public static SharedAdminNote ToShared(this IAdminRemarksRecord note) NoteSeverity? severity = null; var secret = false; NoteType type; - string[]? bannedRoles = null; + ImmutableArray? bannedRoles = null; string? unbannedByName = null; DateTime? unbannedTime = null; bool? seen = null; @@ -30,13 +32,13 @@ public static SharedAdminNote ToShared(this IAdminRemarksRecord note) 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 SharedAdminNote ToShared(this IAdminRemarksRecord note) } // There may be bans without a user, but why would we ever be converting them to shared notes? - if (note.Player is null) - throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); + if (note.Players.Length == 0) + throw new ArgumentNullException(nameof(note), "Player user ID cannot be empty for a note"); return new SharedAdminNote( note.Id, - note.Player!.UserId, - note.Round?.Id, - note.Round?.Server.Name, + [..note.Players.Select(p => p.UserId)], + [..note.Rounds.Select(r => r.Id)], + note.Rounds.SingleOrDefault()?.Server.Name, // TODO: Show all server names? note.PlaytimeAtNote, type, note.Message, diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 412b1911710..f10cd4e3f9c 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -52,7 +52,7 @@ public bool CanView(ICommonSession admin) 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 async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType var note = new SharedAdminNote( noteId, - (NetUserId) player, - roundId, + [(NetUserId) player], + roundId.HasValue ? [roundId.Value] : [], serverName, playtime, type, @@ -172,8 +172,7 @@ public async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType 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 async Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession de 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 async Task ModifyAdminRemark(int noteId, NoteType type, ICommonSession ed case NoteType.Message: await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime); break; - case NoteType.ServerBan: + case NoteType.ServerBan or NoteType.RoleBan: if (severity is null) throw new ArgumentException("Severity cannot be null for a ban", nameof(severity)); - await _db.EditServerBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); - break; - case NoteType.RoleBan: - if (severity is null) - throw new ArgumentException("Severity cannot be null for a role ban", nameof(severity)); - await _db.EditServerRoleBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); + await _db.EditBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); diff --git a/Content.Server/Administration/Notes/IAdminNotesManager.cs b/Content.Server/Administration/Notes/IAdminNotesManager.cs index f54f8a21bda..4e992ba30b1 100644 --- a/Content.Server/Administration/Notes/IAdminNotesManager.cs +++ b/Content.Server/Administration/Notes/IAdminNotesManager.cs @@ -2,6 +2,7 @@ using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; +using Robust.Shared.Network; using Robust.Shared.Player; namespace Content.Server.Administration.Notes; @@ -16,7 +17,7 @@ public interface IAdminNotesManager bool CanDelete(ICommonSession admin); bool CanEdit(ICommonSession admin); bool CanView(ICommonSession admin); - Task OpenEui(ICommonSession admin, Guid notedPlayer); + Task OpenEui(ICommonSession admin, NetUserId notedPlayer); Task OpenUserNotesEui(ICommonSession player); Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime); Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy); diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs index 31acd33bf12..bfc4990b759 100644 --- a/Content.Server/Administration/PlayerPanelEui.cs +++ b/Content.Server/Administration/PlayerPanelEui.cs @@ -186,11 +186,8 @@ public async void SetPlayerState() { _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId); // This won't get associated ip or hwid bans but they were not placed on this account anyways - _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count; - // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally - // The only way to distinguish whether a role ban is the same is to compare the ban time. - // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now. - _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count(); + _bans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null)).Count; + _roleBans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null, type: BanType.Role)).Count(); } else { diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 1ef68727d5f..e6af0d22352 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -172,7 +172,7 @@ private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs } // Check if the user has been banned - var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null); + var ban = await _dbManager.GetBanAsync(null, e.Session.UserId, null, null); if (ban != null) { var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason)); diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index c4b1cfdfc21..fd02a8ba94b 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -210,7 +210,7 @@ session.Status is SessionStatus.Connected or SessionStatus.InGame * TODO: Jesus H Christ what is this utter mess of a function * TODO: Break this apart into is constituent steps. */ - private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( + private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( NetConnectingArgs e) { // Check if banned. @@ -231,7 +231,7 @@ session.Status is SessionStatus.Connected or SessionStatus.InGame return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null); } - var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); + var bans = await _db.GetBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); if (bans.Count > 0) { var firstBan = bans[0]; diff --git a/Content.Server/Database/BanDef.cs b/Content.Server/Database/BanDef.cs new file mode 100644 index 00000000000..d459b475793 --- /dev/null +++ b/Content.Server/Database/BanDef.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Robust.Shared.Configuration; +using Robust.Shared.Network; + + +namespace Content.Server.Database +{ + public sealed class BanDef + { + public int? Id { get; } + public BanType Type { get; } + public ImmutableArray UserIds { get; } + public ImmutableArray<(IPAddress address, int cidrMask)> Addresses { get; } + public ImmutableArray HWIds { get; } + + public DateTimeOffset BanTime { get; } + public DateTimeOffset? ExpirationTime { get; } + public ImmutableArray RoundIds { get; } + public TimeSpan PlaytimeAtNote { get; } + public string Reason { get; } + public NoteSeverity Severity { get; set; } + public NetUserId? BanningAdmin { get; } + public UnbanDef? Unban { get; } + public ServerBanExemptFlags ExemptFlags { get; } + + public ImmutableArray? Roles { get; } + + public BanDef( + int? id, + BanType type, + ImmutableArray userIds, + ImmutableArray<(IPAddress address, int cidrMask)> addresses, + ImmutableArray hwIds, + DateTimeOffset banTime, + DateTimeOffset? expirationTime, + ImmutableArray roundIds, + TimeSpan playtimeAtNote, + string reason, + NoteSeverity severity, + NetUserId? banningAdmin, + UnbanDef? unban, + ServerBanExemptFlags exemptFlags = default, + ImmutableArray? roles = null) + { + if (userIds.Length == 0 && addresses.Length == 0 && hwIds.Length == 0) + { + throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); + } + + addresses = addresses.Select(address => + { + if (address is { address.IsIPv4MappedToIPv6: true } addr) + { + // Fix IPv6-mapped IPv4 addresses + // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. + address = (addr.address.MapToIPv4(), addr.cidrMask - 96); + } + + return address; + }) + .ToImmutableArray(); + + Id = id; + Type = type; + UserIds = userIds; + Addresses = addresses; + HWIds = hwIds; + BanTime = banTime; + ExpirationTime = expirationTime; + RoundIds = roundIds; + PlaytimeAtNote = playtimeAtNote; + Reason = reason; + Severity = severity; + BanningAdmin = banningAdmin; + Unban = unban; + ExemptFlags = exemptFlags; + + switch (Type) + { + case BanType.Server: + if (roles != null) + throw new ArgumentException("Cannot specify roles for server ban types", nameof(roles)); + break; + + case BanType.Role: + if (roles is not { Length: > 0 }) + throw new ArgumentException("Must specify roles for server ban types", nameof(roles)); + if (exemptFlags != 0) + throw new ArgumentException("Role bans cannot have exempt flags", nameof(exemptFlags)); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + + Roles = roles; + } + + public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) + { + string expires; + if (ExpirationTime is { } expireTime) + { + var duration = expireTime - BanTime; + var utc = expireTime.ToUniversalTime(); + expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f"))); + } + else + { + var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal); + expires = !string.IsNullOrWhiteSpace(appeal) + ? loc.GetString("ban-banned-permanent-appeal", ("link", appeal)) + : loc.GetString("ban-banned-permanent"); + } + + return $""" + {loc.GetString("ban-banned-1")} + {loc.GetString("ban-banned-2", ("reason", Reason))} + {expires} + {loc.GetString("ban-banned-3")} + """; + } + } +} diff --git a/Content.Server/Database/BanMatcher.cs b/Content.Server/Database/BanMatcher.cs index f477ccd822a..0302c0dc13a 100644 --- a/Content.Server/Database/BanMatcher.cs +++ b/Content.Server/Database/BanMatcher.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Linq; using System.Net; using Content.Server.IP; using Content.Shared.Database; @@ -7,7 +8,7 @@ namespace Content.Server.Database; /// -/// Implements logic to match a against a player query. +/// Implements logic to match a against a player query. /// /// /// @@ -29,7 +30,7 @@ public static class BanMatcher /// The ban information. /// Information about the player to match against. /// True if the ban matches the provided player info. - public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) + public static bool BanMatches(BanDef ban, in PlayerInfo player) { var exemptFlags = player.ExemptFlags; // Any flag to bypass BlacklistedRange bans. @@ -39,39 +40,44 @@ public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) if ((ban.ExemptFlags & exemptFlags) != 0) return false; + var playerAddr = player.Address; if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP) - && player.Address != null - && ban.Address is not null - && player.Address.IsInSubnet(ban.Address.Value) + && playerAddr != null + && ban.Addresses.Any(addr => playerAddr.IsInSubnet(addr)) && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer)) { return true; } - if (player.UserId is { } id && ban.UserId == id.UserId) + if (player.UserId is { } id && ban.UserIds.Contains(id)) { return true; } - switch (ban.HWId?.Type) + foreach (var banHwid in ban.HWIds) { - case HwidType.Legacy: - if (player.HWId is { Length: > 0 } hwIdVar - && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - { - return true; - } - break; - case HwidType.Modern: - if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) - { - foreach (var hwid in modernHwIdVar) + switch (banHwid.Type) + { + case HwidType.Legacy: + if (player.HWId is { Length: > 0 } hwIdVar + && hwIdVar.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) { - if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - return true; + return true; } - } - break; + + break; + case HwidType.Modern: + if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) + { + foreach (var hwid in modernHwIdVar) + { + if (hwid.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) + return true; + } + } + + break; + } } return false; diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs index 30fba3434b8..63ab45a726b 100644 --- a/Content.Server/Database/DatabaseRecords.cs +++ b/Content.Server/Database/DatabaseRecords.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net; using Content.Shared.Database; using Robust.Shared.Network; @@ -12,9 +13,9 @@ public interface IAdminRemarksRecord { public int Id { get; } - public RoundRecord? Round { get; } + public ImmutableArray Rounds { get; } - public PlayerRecord? Player { get; } + public ImmutableArray Players { get; } public TimeSpan PlaytimeAtNote { get; } public string Message { get; } @@ -31,10 +32,11 @@ public interface IAdminRemarksRecord public bool Deleted { get; } } -public sealed record ServerRoleBanNoteRecord( +public sealed record BanNoteRecord( int Id, - RoundRecord? Round, - PlayerRecord? Player, + BanType Type, + ImmutableArray Rounds, + ImmutableArray Players, TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, @@ -44,25 +46,9 @@ public sealed record ServerRoleBanNoteRecord( DateTimeOffset? LastEditedAt, DateTimeOffset? ExpirationTime, bool Deleted, - string[] Roles, PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; - -public sealed record ServerBanNoteRecord( - int Id, - RoundRecord? Round, - PlayerRecord? Player, - TimeSpan PlaytimeAtNote, - string Message, - NoteSeverity Severity, - PlayerRecord? CreatedBy, - DateTimeOffset CreatedAt, - PlayerRecord? LastEditedBy, - DateTimeOffset? LastEditedAt, - DateTimeOffset? ExpirationTime, - bool Deleted, - PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; + DateTime? UnbanTime, + ImmutableArray Roles) : IAdminRemarksRecord; public sealed record AdminNoteRecord( int Id, @@ -79,7 +65,11 @@ public sealed record AdminNoteRecord( bool Deleted, PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, - bool Secret) : IAdminRemarksRecord; + bool Secret) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminWatchlistRecord( int Id, @@ -94,7 +84,11 @@ public sealed record AdminWatchlistRecord( DateTimeOffset? ExpirationTime, bool Deleted, PlayerRecord? DeletedBy, - DateTimeOffset? DeletedAt) : IAdminRemarksRecord; + DateTimeOffset? DeletedAt) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminMessageRecord( int Id, @@ -111,15 +105,18 @@ public sealed record AdminMessageRecord( PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, bool Seen, - bool Dismissed) : IAdminRemarksRecord; - + bool Dismissed) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record PlayerRecord( NetUserId UserId, DateTimeOffset FirstSeenTime, string LastSeenUserName, DateTimeOffset LastSeenTime, - IPAddress LastSeenAddress, + IPAddress? LastSeenAddress, ImmutableTypedHwid? HWId); public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server); diff --git a/Content.Server/Database/EFCoreExtensions.cs b/Content.Server/Database/EFCoreExtensions.cs new file mode 100644 index 00000000000..9c79b7f6d37 --- /dev/null +++ b/Content.Server/Database/EFCoreExtensions.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace Content.Server.Database; + +internal static class EFCoreExtensions +{ + public static IQueryable ApplyIncludes( + this IQueryable query, + IEnumerable>> properties) + where TEntity : class + { + var q = query; + foreach (var property in properties) + { + q = q.Include(property); + } + + return q; + } + + public static IQueryable ApplyIncludes( + this IQueryable query, + IEnumerable>> properties, + Expression> getDerived) + where TEntity : class + where TDerived : class + { + var q = query; + foreach (var property in properties) + { + q = q.Include(getDerived).ThenInclude(property); + } + + return q; + } +} diff --git a/Content.Server/Database/ServerBanDef.cs b/Content.Server/Database/ServerBanDef.cs deleted file mode 100644 index eb03227e1ea..00000000000 --- a/Content.Server/Database/ServerBanDef.cs +++ /dev/null @@ -1,94 +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")} - {loc.GetString("ban-banned-4")} - """; - } - } -} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index deac545b4f6..2bcf2505414 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -1,13 +1,13 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; using System.Net; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Logs; -using Content.Server.Administration.Managers; using Content.Shared._Common.Consent; // Consent system using Content.Shared.Administration.Logs; using Content.Shared.Construction.Prototypes; @@ -413,7 +413,7 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// /// The ban id to look for. /// The ban with the given id or null if none exist. - public abstract Task GetServerBanAsync(int id); + public abstract Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -425,11 +425,12 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// The legacy HWId of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - public abstract Task GetServerBanAsync( + public abstract Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type); /// /// Looks up an user's ban history. @@ -442,17 +443,18 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// The modern HWIDs of the user. /// Include pardoned and expired bans. /// The user's ban history. - public abstract Task> GetServerBansAsync( + public abstract Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned); + bool includeUnbanned, + BanType type); - public abstract Task AddServerBanAsync(ServerBanDef serverBan); - public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); + public abstract Task AddBanAsync(BanDef ban); + public abstract Task AddUnbanAsync(UnbanDef unban); - public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public async Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -515,61 +517,23 @@ public async Task GetBanExemption(NetUserId userId, Cancel return flags ?? ServerBanExemptFlags.None; } - #endregion - - #region Role Bans - /* - * ROLE BANS - */ - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - public abstract Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - public abstract Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned); - - public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); - public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban); - - public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + protected static List>> GetBanDefIncludes(BanType? type = null) { - await using var db = await GetDb(); - var roleBanDetails = await db.DbContext.RoleBan - .Where(b => b.Id == id) - .Select(b => new { b.BanTime, b.PlayerUserId }) - .SingleOrDefaultAsync(); + List>> list = + [ + b => b.Players!, + b => b.Rounds!, + b => b.Hwids!, + b => b.Unban!, + b => b.Addresses!, + ]; - if (roleBanDetails == default) - return; + if (type != BanType.Server) + list.Add(b => b.Roles!); - await db.DbContext.RoleBan - .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId) - .ExecuteUpdateAsync(setters => setters - .SetProperty(b => b.Severity, severity) - .SetProperty(b => b.Reason, reason) - .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null) - .SetProperty(b => b.LastEditedById, editedBy) - .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime) - ); + return list; } + #endregion #region Playtime @@ -698,6 +662,19 @@ protected async Task PlayerRecordExists(DbGuard db, NetUserId userId) 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)), @@ -721,7 +698,7 @@ public abstract Task AddConnectionLogAsync(NetUserId userId, ConnectionDenyReason? denied, int serverId); - public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) { await using var db = await GetDb(); @@ -1363,81 +1340,17 @@ private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity) entity.Dismissed); } - public async Task GetServerBanAsNoteAsync(int id) + public async Task GetBanAsNoteAsync(int id) { await using var db = await GetDb(); - var ban = await db.DbContext.Ban - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .SingleOrDefaultAsync(b => b.Id == id); - - if (ban is null) - return null; - - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - return new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - ban.BanTime, - MakePlayerRecord(ban.LastEditedBy), - ban.LastEditedAt, - ban.ExpirationTime, - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync(p => - p.UserId == ban.Unban.UnbanningAdmin.Value)), - ban.Unban?.UnbanTime); - } - - public async Task GetServerRoleBanAsNoteAsync(int id) - { - await using var db = await GetDb(); - - var ban = await db.DbContext.RoleBan - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + 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); - 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.JobPrefix, null) }, - MakePlayerRecord(unbanningAdmin), - ban.Unban?.UnbanTime); + return await MakeBanNoteRecord(db.DbContext, ban); } public async Task> GetAllAdminRemarks(Guid player) @@ -1458,8 +1371,7 @@ public async Task> GetAllAdminRemarks(Guid player) .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) @@ -1542,7 +1454,7 @@ public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset dele 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(); @@ -1555,19 +1467,6 @@ public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset await db.DbContext.SaveChangesAsync(); } - public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - await using var db = await GetDb(); - - var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync(); - - roleBan.Hidden = true; - roleBan.LastEditedById = deletedBy; - roleBan.LastEditedAt = deletedAt.UtcDateTime; - - await db.DbContext.SaveChangesAsync(); - } - public async Task> GetVisibleAdminRemarks(Guid player) { await using var db = await GetDb(); @@ -1585,8 +1484,7 @@ public async Task> GetVisibleAdminRemarks(Guid player) .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; } @@ -1649,98 +1547,70 @@ public async Task MarkMessageAsSeen(int id, bool dismissedToo) await db.DbContext.SaveChangesAsync(); } - // These two are here because they get converted into notes later - protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) + private static IQueryable BanRecordQuery(ServerDbContext dbContext) { - // You can't group queries, as player will not always exist. When it doesn't, the - // whole query returns nothing - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - var bans = await db.DbContext.Ban - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) + return dbContext.Ban .Include(ban => ban.Unban) - .Include(ban => ban.Round) + .Include(ban => ban.Rounds!) + .ThenInclude(r => r.Round) .ThenInclude(r => r!.Server) + .Include(ban => ban.Addresses) + .Include(ban => ban.Players) + .Include(ban => ban.Roles) + .Include(ban => ban.Hwids) .Include(ban => ban.CreatedBy) .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .ToArrayAsync(); - - var banNotes = new List(); - foreach (var ban in bans) - { - var banNote = new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - NormalizeDatabaseTime(ban.BanTime), - MakePlayerRecord(ban.LastEditedBy), - NormalizeDatabaseTime(ban.LastEditedAt), - NormalizeDatabaseTime(ban.ExpirationTime), - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync( - p => p.UserId == ban.Unban.UnbanningAdmin.Value)), - NormalizeDatabaseTime(ban.Unban?.UnbanTime)); + .Include(ban => ban.Unban); + } - banNotes.Add(banNote); - } + private async Task MakeBanNoteRecord(ServerDbContext dbContext, Ban ban) + { + var playerRecords = await AsyncSelect(ban.Players, + async bp => MakePlayerRecord(bp.UserId, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == bp.UserId))); - return banNotes; + return new BanNoteRecord( + ban.Id, + ban.Type, + [..ban.Rounds!.Select(br => MakeRoundRecord(br.Round!))], + [..playerRecords], + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy!), + NormalizeDatabaseTime(ban.BanTime), + MakePlayerRecord(ban.LastEditedBy!), + NormalizeDatabaseTime(ban.LastEditedAt), + NormalizeDatabaseTime(ban.ExpirationTime), + ban.Hidden, + ban.Unban?.UnbanningAdmin == null + ? null + : MakePlayerRecord( + ban.Unban.UnbanningAdmin.Value, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.Unban.UnbanningAdmin.Value)), + NormalizeDatabaseTime(ban.Unban?.UnbanTime), + [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]); } - protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) + // These two are here because they get converted into notes later + protected async Task> GetBansAsNotesForUser(DbGuard db, Guid user) { - // Server side query - var bansQuery = await db.DbContext.RoleBan - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + // You can't group queries, as player will not always exist. When it doesn't, the + // whole query returns nothing + var bans = await BanRecordQuery(db.DbContext) + .AsSplitQuery() + .Where(ban => ban.Players!.Any(bp => bp.UserId == user) && !ban.Hidden) .ToArrayAsync(); - // Client side query, as EF can't do groups yet - var bansEnumerable = bansQuery - .GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null }) - .Select(banGroup => banGroup) - .ToArray(); - - List bans = new(); - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - foreach (var banGroup in bansEnumerable) + var banNotes = new List(); + foreach (var ban in bans) { - var firstBan = banGroup.First(); - Player? unbanningAdmin = null; - - if (firstBan.Unban?.UnbanningAdmin is not null) - unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value); - - bans.Add(new ServerRoleBanNoteRecord( - firstBan.Id, - MakeRoundRecord(firstBan.Round), - MakePlayerRecord(player), - firstBan.PlaytimeAtNote, - firstBan.Reason, - firstBan.Severity, - MakePlayerRecord(firstBan.CreatedBy), - NormalizeDatabaseTime(firstBan.BanTime), - MakePlayerRecord(firstBan.LastEditedBy), - NormalizeDatabaseTime(firstBan.LastEditedAt), - NormalizeDatabaseTime(firstBan.ExpirationTime), - firstBan.Hidden, - banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), - MakePlayerRecord(unbanningAdmin), - NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); + var banNote = await MakeBanNoteRecord(db.DbContext, ban); + + banNotes.Add(banNote); } - return bans; + return banNotes; } #endregion @@ -2097,5 +1967,19 @@ public virtual void Shutdown() { } + + private static async Task> AsyncSelect( + IEnumerable? enumerable, + Func> selector) + { + var results = new List(); + + foreach (var item in enumerable ?? []) + { + results.Add(await selector(item)); + } + + return [..results]; + } } } diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 1ab4ea88d2e..3d9cc1f2ff9 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -63,7 +63,7 @@ Task InitPrefsAsync( /// /// The ban id to look for. /// The ban with the given id or null if none exist. - Task GetServerBanAsync(int id); + Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -75,11 +75,12 @@ Task InitPrefsAsync( /// The legacy HWID of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - Task GetServerBanAsync( + Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type = BanType.Server); /// /// Looks up an user's ban history. @@ -91,17 +92,18 @@ Task InitPrefsAsync( /// The modern HWIDs of the user. /// If true, bans that have been expired or pardoned are also included. /// The user's ban history. - Task> GetServerBansAsync( + Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true); + bool includeUnbanned=true, + BanType type = BanType.Server); - Task AddServerBanAsync(ServerBanDef serverBan); - Task AddServerUnbanAsync(ServerUnbanDef serverBan); + Task AddBanAsync(BanDef ban); + Task AddUnbanAsync(UnbanDef ban); - public Task EditServerBan( + public Task EditBan( int id, string reason, NoteSeverity severity, @@ -127,45 +129,6 @@ public Task EditServerBan( #endregion - #region Role Bans - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true); - - Task AddServerRoleBanAsync(ServerRoleBanDef serverBan); - Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan); - - public Task EditServerRoleBan( - int id, - string reason, - NoteSeverity severity, - DateTimeOffset? expiration, - Guid editedBy, - DateTimeOffset editedAt); - #endregion - #region Playtime /// @@ -205,7 +168,7 @@ Task AddConnectionLogAsync( ConnectionDenyReason? denied, int serverId); - Task AddServerBanHitsAsync(int connection, IEnumerable bans); + Task AddServerBanHitsAsync(int connection, IEnumerable bans); #endregion @@ -297,8 +260,7 @@ Task AddConnectionLogAsync( Task GetAdminNote(int id); Task GetAdminWatchlist(int id); Task GetAdminMessage(int id); - Task GetServerBanAsNoteAsync(int id); - Task GetServerRoleBanAsNoteAsync(int id); + Task GetBanAsNoteAsync(int id); Task> GetAllAdminRemarks(Guid player); Task> GetVisibleAdminNotes(Guid player); Task> GetActiveWatchlists(Guid player); @@ -309,8 +271,7 @@ Task AddConnectionLogAsync( Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); /// /// Mark an admin message as being seen by the target player. @@ -522,49 +483,51 @@ public Task AssignUserIdAsync(string name, NetUserId userId) return RunDbCommand(() => _db.GetAssignedUserIdAsync(name)); } - public Task GetServerBanAsync(int id) + public Task GetBanAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(id)); + return RunDbCommand(() => _db.GetBanAsync(id)); } - public Task GetServerBanAsync( + public Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds)); + return RunDbCommand(() => _db.GetBanAsync(address, userId, hwId, modernHWIds, type)); } - public Task> GetServerBansAsync( + public Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true) + bool includeUnbanned=true, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); + return RunDbCommand(() => _db.GetBansAsync(address, userId, hwId, modernHWIds, includeUnbanned, type)); } - public Task AddServerBanAsync(ServerBanDef serverBan) + public Task AddBanAsync(BanDef ban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerBanAsync(serverBan)); + return RunDbCommand(() => _db.AddBanAsync(ban)); } - public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) + public Task AddUnbanAsync(UnbanDef unban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); + return RunDbCommand(() => _db.AddUnbanAsync(unban)); } - public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); + return RunDbCommand(() => _db.EditBan(id, reason, severity, expiration, editedBy, editedAt)); } public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags) @@ -579,43 +542,6 @@ public Task GetBanExemption(NetUserId userId, Cancellation return RunDbCommand(() => _db.GetBanExemption(userId, cancel)); } - #region Role Ban - public Task GetServerRoleBanAsync(int id) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsync(id)); - } - - public Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); - } - - public Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan)); - } - - public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban)); - } - - public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt)); - } - #endregion - #region Playtime public Task> GetPlayTimes(Guid player, CancellationToken cancel) @@ -667,7 +593,7 @@ public Task AddConnectionLogAsync( return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId)); } - public Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public Task AddServerBanHitsAsync(int connection, IEnumerable bans) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans)); @@ -928,16 +854,10 @@ public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtN return RunDbCommand(() => _db.GetAdminMessage(id)); } - public Task GetServerBanAsNoteAsync(int id) + public Task GetBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); - } - - public Task GetServerRoleBanAsNoteAsync(int id) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id)); + return RunDbCommand(() => _db.GetBanAsNoteAsync(id)); } public Task> GetAllAdminRemarks(Guid player) @@ -999,16 +919,10 @@ public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); } - public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); - } - - public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) + public Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt)); + return RunDbCommand(() => _db.HideBanFromNotes(id, deletedBy, deletedAt)); } public Task MarkMessageAsSeen(int id, bool dismissedToo) diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index c0346708377..005624747e1 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -58,24 +58,26 @@ public ServerDbPostgres(DbContextOptions options, } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var query = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(p => p.Id == id); + .ApplyIncludes(GetBanDefIncludes()) + .Where(p => p.Id == id) + .AsSplitQuery(); var ban = await query.SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -86,7 +88,7 @@ public ServerDbPostgres(DbContextOptions options, 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(); @@ -94,11 +96,12 @@ public ServerDbPostgres(DbContextOptions options, return ConvertBan(ban); } - public override async Task> GetServerBansAsync(IPAddress? address, + public override async Task> GetBansAsync(IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -107,12 +110,11 @@ public override async Task> GetServerBansAsync(IPAddress? add await using var db = await GetDbImpl(); - var exempt = await GetBanExemptionCore(db, userId); + var exempt = type == BanType.Role ? null : await GetBanExemptionCore(db, userId); var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId); - var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer); - + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer, type); var queryBans = await query.ToArrayAsync(); - var bans = new List(queryBans.Length); + var bans = new List(queryBans.Length); foreach (var ban in queryBans) { @@ -127,7 +129,8 @@ public override async Task> GetServerBansAsync(IPAddress? add return bans; } - private static IQueryable MakeBanLookupQuery( + // This has to return IDs instead of direct objects because otherwise all the includes are too complicated. + private static IQueryable MakeBanLookupQuery( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, @@ -135,308 +138,113 @@ private static IQueryable MakeBanLookupQuery( DbGuardImpl db, bool includeUnbanned, ServerBanExemptFlags? exemptFlags, - bool newPlayer) + bool newPlayer, + BanType type) { DebugTools.Assert(!(address == null && userId == null && hwId == null)); - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.Ban); - - if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)) - { - var newQ = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(b => b.Address != null - && EF.Functions.ContainsOrEqual(b.Address.Value, address) - && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer)); - - query = query == null ? newQ : query.Union(newQ); - } - - DebugTools.Assert( - query != null, - "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - if (exemptFlags is { } exempt) - { - if (exempt != ServerBanExemptFlags.None) - exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - - query = query.Where(b => (b.ExemptFlags & exempt) == 0); - } - - return query.Distinct(); - } - - private static IQueryable? MakeBanLookupQualityShared( - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbSet set) - where TBan : class, IBanCommon - where TUnban : class, IUnbanCommon - { - IQueryable? query = null; + var selectorQueries = new List>(); if (userId is { } uid) - { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.PlayerUserId == uid.UserId); - - query = query == null ? newQ : query.Union(newQ); - } + selectorQueries.Add(db.DbContext.BanPlayer.Where(b => b.UserId == uid.UserId)); if (hwId != null && hwId.Value.Length > 0) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid.Where(bh => + bh.HWId!.Type == HwidType.Legacy && bh.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()) + )); } if (modernHWIds != null) { foreach (var modernHwid in modernHWIds) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid + .Where(b => b.HWId!.Type == HwidType.Modern + && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()))); } } - return query; - } - - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - - NetUserId? aUid = null; - if (ban.BanningAdmin is {} aGuid) + if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None) + .HasFlag(ServerBanExemptFlags.IP)) { - aUid = new NetUserId(aGuid); + selectorQueries.Add(db.PgDbContext.BanAddress + .Where(ba => EF.Functions.ContainsOrEqual(ba.Address, address) + && !(ba.Ban!.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && + !newPlayer))); } - var unbanDef = ConvertUnban(ban.Unban); - - return new ServerBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - ban.BanTime, - ban.ExpirationTime, - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unbanDef, - ban.ExemptFlags); - } - - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is {} aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerUnbanDef( - unban.Id, - aUid, - unban.UnbanTime); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - HWId = serverBan.HWId, - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.PgDbContext.SaveChangesAsync(); - } - - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.PgDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var query = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id); - - var ban = await query.SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - - } - - public override async Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) - { - if (address == null && userId == null && hwId == null) - { - throw new ArgumentException("Address, userId, and hwId cannot all be null"); - } - - await using var db = await GetDbImpl(); - - var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned) - .OrderByDescending(b => b.BanTime); + DebugTools.Assert( + selectorQueries.Count > 0, + "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - return await QueryRoleBans(query); - } + var selectorQuery = selectorQueries + .Select(q => q.Select(sel => sel.BanId)) + .Aggregate((selectors, queryable) => selectors.Union(queryable)); - private static async Task> QueryRoleBans(IQueryable query) - { - var queryRoleBans = await query.ToArrayAsync(); - var bans = new List(queryRoleBans.Length); + var banQuery = db.DbContext.Ban.Where(b => selectorQuery.Contains(b.Id)); - foreach (var ban in queryRoleBans) + if (!includeUnbanned) { - var banDef = ConvertRoleBan(ban); - - if (banDef != null) - { - bans.Add(banDef); - } + banQuery = banQuery.Where(p => + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } - return bans; - } - - private static IQueryable MakeRoleBanLookupQuery( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbGuardImpl db, - bool includeUnbanned) - { - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.RoleBan); - - if (address != null) + if (exemptFlags is { } exempt) { - var newQ = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address)); + if (exempt != ServerBanExemptFlags.None) + exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - query = query == null ? newQ : query.Union(newQ); + banQuery = banQuery.Where(b => (b.ExemptFlags & exempt) == 0); } - if (!includeUnbanned) - { - query = query?.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - query = query!.Distinct(); - return query; + return banQuery + .Where(b => b.Type == type) + .ApplyIncludes(GetBanDefIncludes(type)) + .AsSplitQuery(); } [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is {} aGuid) { aUid = new NetUserId(aGuid); } - var unbanDef = ConvertRoleUnban(ban.Unban); + var unbanDef = ConvertUnban(ban.Unban); - return new ServerRoleBanDef( + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) + { + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; + } + + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], ban.BanTime, ban.ExpirationTime, - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, unbanDef, - ban.RoleId); + ban.ExemptFlags, + roles); } - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -449,45 +257,54 @@ private static IQueryable MakeRoleBanLookupQuery( aUid = new NetUserId(aGuid); } - return new ServerRoleUnbanDef( + return new UnbanDef( unban.Id, aUid, unban.UnbanTime); } - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - var ban = new ServerRoleBan - { - Address = serverRoleBan.Address.ToNpgsqlInet(), - HWId = serverRoleBan.HWId, - Reason = serverRoleBan.Reason, - Severity = serverRoleBan.Severity, - BanningAdmin = serverRoleBan.BanningAdmin?.UserId, - BanTime = serverRoleBan.BanTime.UtcDateTime, - ExpirationTime = serverRoleBan.ExpirationTime?.UtcDateTime, - RoundId = serverRoleBan.RoundId, - PlaytimeAtNote = serverRoleBan.PlaytimeAtNote, - PlayerUserId = serverRoleBan.UserId?.UserId, - RoleId = serverRoleBan.Role, + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole + { + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.PgDbContext.RoleBan.Add(ban); + db.PgDbContext.Ban.Add(banEntity); await db.PgDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.PgDbContext.RoleUnban.Add(new ServerRoleUnban + db.PgDbContext.Unban.Add(new Unban { - BanId = serverRoleUnban.BanId, - UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId, - UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.PgDbContext.SaveChangesAsync(); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index c3109ec6e66..6ccd4c5e283 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -66,48 +66,52 @@ public ServerDbSqlite( } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var ban = await db.SqliteDbContext.Ban - .Include(p => p.Unban) + .ApplyIncludes(GetBanDefIncludes()) .Where(p => p.Id == id) + .AsSplitQuery() .SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false, type)).FirstOrDefault(); } - public override async Task> GetServerBansAsync( + public override async Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned, type)).ToList(); } - private async Task> GetServerBanQueryAsync( + private async Task> GetBanQueryAsync( DbGuardImpl db, IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { var exempt = await GetBanExemptionCore(db, userId); @@ -115,7 +119,7 @@ private async Task> GetServerBanQueryAsync( // 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 { @@ -132,12 +136,12 @@ private async Task> GetServerBanQueryAsync( .Where(b => BanMatcher.BanMatches(b!, playerInfo))!; } - private static async Task> GetAllBans( - SqliteServerDbContext db, + private static async Task> GetAllBans(SqliteServerDbContext db, bool includeUnbanned, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + BanType type) { - IQueryable query = db.Ban.Include(p => p.Unban); + var query = db.Ban.Where(b => b.Type == type).ApplyIncludes(GetBanDefIncludes(type)); if (!includeUnbanned) { query = query.Where(p => @@ -153,269 +157,99 @@ private static async Task> GetAllBans( query = query.Where(b => (b.ExemptFlags & exempt) == 0); } - return await query.ToListAsync(); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.SqliteDbContext.SaveChangesAsync(); + return await query.AsSplitQuery().ToListAsync(); } - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.SqliteDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var ban = await db.SqliteDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id) - .SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - } - - public override async Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - // SQLite can't do the net masking stuff we need to match IP address ranges. - // So just pull down the whole list into memory. - var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned); - - return queryBans - .Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds)) - .Select(ConvertRoleBan) - .ToList()!; - } - - private static async Task> GetAllRoleBans( - SqliteServerDbContext db, - bool includeUnbanned) - { - IQueryable query = db.RoleBan.Include(p => p.Unban); - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - return await query.ToListAsync(); - } - - private static bool RoleBanMatches( - ServerRoleBan ban, - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds) - { - if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) - { - return true; - } - - if (userId is { } id && ban.PlayerUserId == id.UserId) - { - return true; - } - - switch (ban.HWId?.Type) - { - case HwidType.Legacy: - if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - break; - - case HwidType.Modern: - if (modernHWIds != null) - { - foreach (var modernHWId in modernHWIds) + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole { - if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - } - } - - break; - } - - return false; - } - - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverBan) - { - await using var db = await GetDbImpl(); - - var ban = new ServerRoleBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - RoleId = serverBan.Role, + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.SqliteDbContext.RoleBan.Add(ban); + db.SqliteDbContext.Ban.Add(banEntity); await db.SqliteDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban + db.SqliteDbContext.Unban.Add(new Unban { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.SqliteDbContext.SaveChangesAsync(); } + #endregion [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is { } aGuid) { aUid = new NetUserId(aGuid); } - var unban = ConvertRoleUnban(ban.Unban); - - return new ServerRoleBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), - ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unban, - ban.RoleId); - } - - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is { } aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerRoleUnbanDef( - unban.Id, - aUid, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(unban.UnbanTime, DateTimeKind.Utc)); - } - #endregion - - [return: NotNullIfNotNull(nameof(ban))] - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } + var unban = ConvertUnban(ban.Unban); - NetUserId? aUid = null; - if (ban.BanningAdmin is { } aGuid) + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) { - aUid = new NetUserId(aGuid); + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; } - var unban = ConvertUnban(ban.Unban); - - return new ServerBanDef( + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, - unban); + unban, + ban.ExemptFlags, + roles); } - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -428,7 +262,7 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba aUid = new NetUserId(aGuid); } - return new ServerUnbanDef( + return new UnbanDef( unban.Id, aUid, // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. diff --git a/Content.Server/Database/ServerRoleBanDef.cs b/Content.Server/Database/ServerRoleBanDef.cs deleted file mode 100644 index dda3a822378..00000000000 --- a/Content.Server/Database/ServerRoleBanDef.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Net; -using Content.Shared.Database; -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleBanDef -{ - public int? Id { get; } - public NetUserId? UserId { get; } - public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableTypedHwid? HWId { get; } - - public DateTimeOffset BanTime { get; } - public DateTimeOffset? ExpirationTime { get; } - public int? RoundId { get; } - public TimeSpan PlaytimeAtNote { get; } - public string Reason { get; } - public NoteSeverity Severity { get; set; } - public NetUserId? BanningAdmin { get; } - public ServerRoleUnbanDef? Unban { get; } - public string Role { get; } - - public ServerRoleBanDef( - int? id, - NetUserId? userId, - (IPAddress, int)? address, - ImmutableTypedHwid? hwId, - DateTimeOffset banTime, - DateTimeOffset? expirationTime, - int? roundId, - TimeSpan playtimeAtNote, - string reason, - NoteSeverity severity, - NetUserId? banningAdmin, - ServerRoleUnbanDef? unban, - string role) - { - if (userId == null && address == null && hwId == null) - { - throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); - } - - if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6) - { - // Fix IPv6-mapped IPv4 addresses - // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - address = (addr.Item1.MapToIPv4(), addr.Item2 - 96); - } - - Id = id; - UserId = userId; - Address = address; - HWId = hwId; - BanTime = banTime; - ExpirationTime = expirationTime; - RoundId = roundId; - PlaytimeAtNote = playtimeAtNote; - Reason = reason; - Severity = severity; - BanningAdmin = banningAdmin; - Unban = unban; - Role = role; - } -} diff --git a/Content.Server/Database/ServerRoleUnbanDef.cs b/Content.Server/Database/ServerRoleUnbanDef.cs deleted file mode 100644 index 3960a86808f..00000000000 --- a/Content.Server/Database/ServerRoleUnbanDef.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleUnbanDef -{ - public int BanId { get; } - - public NetUserId? UnbanningAdmin { get; } - - public DateTimeOffset UnbanTime { get; } - - public ServerRoleUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) - { - BanId = banId; - UnbanningAdmin = unbanningAdmin; - UnbanTime = unbanTime; - } -} diff --git a/Content.Server/Database/ServerUnbanDef.cs b/Content.Server/Database/UnbanDef.cs similarity index 72% rename from Content.Server/Database/ServerUnbanDef.cs rename to Content.Server/Database/UnbanDef.cs index 3d39a6b90c4..1fa4fc2a6a9 100644 --- a/Content.Server/Database/ServerUnbanDef.cs +++ b/Content.Server/Database/UnbanDef.cs @@ -2,7 +2,7 @@ namespace Content.Server.Database { - public sealed class ServerUnbanDef + public sealed class UnbanDef { public int BanId { get; } @@ -10,7 +10,7 @@ public sealed class ServerUnbanDef public DateTimeOffset UnbanTime { get; } - public ServerUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) + public UnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) { BanId = banId; UnbanningAdmin = unbanningAdmin; diff --git a/Content.Server/IP/IPAddressExt.cs b/Content.Server/IP/IPAddressExt.cs index a61477e01bc..8c514e96b91 100644 --- a/Content.Server/IP/IPAddressExt.cs +++ b/Content.Server/IP/IPAddressExt.cs @@ -11,22 +11,14 @@ public static class IPAddressExt { // Npgsql used to map inet types as a tuple like this. // I'm upgrading the dependencies and I don't wanna rewrite a bunch of DB code, so a few helpers it shall be. - [return: NotNullIfNotNull(nameof(tuple))] - public static NpgsqlInet? ToNpgsqlInet(this (IPAddress, int)? tuple) + public static NpgsqlInet ToNpgsqlInet(this (IPAddress, int) tuple) { - if (tuple == null) - return null; - - return new NpgsqlInet(tuple.Value.Item1, (byte) tuple.Value.Item2); + return new NpgsqlInet(tuple.Item1, (byte)tuple.Item2); } - [return: NotNullIfNotNull(nameof(inet))] - public static (IPAddress, int)? ToTuple(this NpgsqlInet? inet) + public static (IPAddress, int) ToTuple(this NpgsqlInet inet) { - if (inet == null) - return null; - - return (inet.Value.Address, inet.Value.Netmask); + return (inet.Address, inet.Netmask); } // Taken from https://stackoverflow.com/a/56461160/4678631 diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 9a39f3ace73..5e3fbef10b5 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -385,14 +385,7 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) } 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)) { @@ -558,7 +551,15 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration); - _bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, Loc.GetString("votekick-ban-reason", ("reason", reason))); + var banInfo = new CreateServerBanInfo(Loc.GetString("votekick-ban-reason", ("reason", reason))); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + banInfo.AddAddress(targetIP); + banInfo.WithSeverity(severity); + if (minutes > 0) + banInfo.WithMinutes(minutes); + + _bans.CreateServerBan(banInfo); } } else diff --git a/Content.Shared.Database/Bans.cs b/Content.Shared.Database/Bans.cs new file mode 100644 index 00000000000..9fb100f5db4 --- /dev/null +++ b/Content.Shared.Database/Bans.cs @@ -0,0 +1,33 @@ +namespace Content.Shared.Database; + +/// +/// Types of bans that can be stored in the database. +/// +public enum BanType : byte +{ + /// + /// A ban from the entire server. If a player matches the ban info, they will be refused connection. + /// + Server, + + /// + /// A ban from playing one or more roles. + /// + Role, +} + +/// +/// A single role for a database role ban. +/// +/// The type of role being banned, e.g. Job. +/// +/// The ID of the role being banned. This is likely a prototype ID based on . +/// +[Serializable] +public record struct BanRoleDef(string RoleType, string RoleId) +{ + public override string ToString() + { + return $"{RoleType}:{RoleId}"; + } +} diff --git a/Content.Shared/Administration/BanList/BanListEuiState.cs b/Content.Shared/Administration/BanList/BanListEuiState.cs index 09faa9706ea..c885ff1f706 100644 --- a/Content.Shared/Administration/BanList/BanListEuiState.cs +++ b/Content.Shared/Administration/BanList/BanListEuiState.cs @@ -6,7 +6,7 @@ namespace Content.Shared.Administration.BanList; [Serializable, NetSerializable] public sealed class BanListEuiState : EuiStateBase { - public BanListEuiState(string banListPlayerName, List bans, List roleBans) + public BanListEuiState(string banListPlayerName, List bans, List roleBans) { BanListPlayerName = banListPlayerName; Bans = bans; @@ -14,6 +14,6 @@ public BanListEuiState(string banListPlayerName, List bans, Lis } public string BanListPlayerName { get; } - public List Bans { get; } - public List RoleBans { get; } + public List Bans { get; } + public List RoleBans { get; } } diff --git a/Content.Shared/Administration/BanList/SharedBan.cs b/Content.Shared/Administration/BanList/SharedBan.cs new file mode 100644 index 00000000000..68ccc02c58f --- /dev/null +++ b/Content.Shared/Administration/BanList/SharedBan.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; +using Content.Shared.Database; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration.BanList; + +[Serializable, NetSerializable] +public record SharedBan( + int? Id, + BanType Type, + ImmutableArray UserIds, + ImmutableArray<(string address, int cidrMask)> Addresses, + ImmutableArray HWIds, + DateTime BanTime, + DateTime? ExpirationTime, + string Reason, + string? BanningAdminName, + SharedUnban? Unban, + ImmutableArray? Roles); diff --git a/Content.Shared/Administration/BanList/SharedServerBan.cs b/Content.Shared/Administration/BanList/SharedServerBan.cs deleted file mode 100644 index a8b9ce0d9a1..00000000000 --- a/Content.Shared/Administration/BanList/SharedServerBan.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public record SharedServerBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban -); diff --git a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs b/Content.Shared/Administration/BanList/SharedServerRoleBan.cs deleted file mode 100644 index fca2ea15839..00000000000 --- a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public sealed record SharedServerRoleBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban, - string Role -) : SharedServerBan(Id, UserId, Address, HWId, BanTime, ExpirationTime, Reason, BanningAdminName, Unban); diff --git a/Content.Shared/Administration/BanList/SharedServerUnban.cs b/Content.Shared/Administration/BanList/SharedUnban.cs similarity index 81% rename from Content.Shared/Administration/BanList/SharedServerUnban.cs rename to Content.Shared/Administration/BanList/SharedUnban.cs index f3a57e41590..d60bb9184e2 100644 --- a/Content.Shared/Administration/BanList/SharedServerUnban.cs +++ b/Content.Shared/Administration/BanList/SharedUnban.cs @@ -3,7 +3,7 @@ namespace Content.Shared.Administration.BanList; [Serializable, NetSerializable] -public sealed record SharedServerUnban( +public sealed record SharedUnban( string? UnbanningAdmin, DateTime UnbanTime ); diff --git a/Content.Shared/Administration/Notes/SharedAdminNote.cs b/Content.Shared/Administration/Notes/SharedAdminNote.cs index 09d4f3f9478..d849d350784 100644 --- a/Content.Shared/Administration/Notes/SharedAdminNote.cs +++ b/Content.Shared/Administration/Notes/SharedAdminNote.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Content.Shared.Database; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -7,8 +8,8 @@ namespace Content.Shared.Administration.Notes; [Serializable, NetSerializable] public sealed record SharedAdminNote( int Id, // Id of note, message, watchlist, ban or role ban. Should be paired with NoteType to uniquely identify a shared admin note. - NetUserId Player, // Notes player - int? Round, // Which round was it added in? + ImmutableArray Players, // Notes player + ImmutableArray Rounds, // Which round was it added in? string? ServerName, // Which server was this added on? TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note NoteType NoteType, // Type of note @@ -20,7 +21,7 @@ public sealed record SharedAdminNote( DateTime CreatedAt, // When was it created? DateTime? LastEditedAt, // When was it last edited? DateTime? ExpiryTime, // Does it expire? - string[]? BannedRoles, // Only valid for role bans. List of banned roles + ImmutableArray? BannedRoles, // Only valid for role bans. List of banned roles DateTime? UnbannedTime, // Only valid for bans. Set if unbanned string? UnbannedByName, // Only valid for bans. Set if unbanned bool? Seen // Only valid for messages, otherwise should be null. Has the user seen this message? diff --git a/Content.Shared/Players/MsgRoleBans.cs b/Content.Shared/Players/MsgRoleBans.cs index fd90f62b0b5..015abb1d308 100644 --- a/Content.Shared/Players/MsgRoleBans.cs +++ b/Content.Shared/Players/MsgRoleBans.cs @@ -1,5 +1,7 @@ +using Content.Shared.Roles; using Lidgren.Network; using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Players; @@ -11,26 +13,38 @@ public sealed class MsgRoleBans : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public List Bans = new(); + public List> JobBans = new(); + public List> AntagBans = new(); public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - var count = buffer.ReadVariableInt32(); - Bans.EnsureCapacity(count); + JobBans.Clear(); + var jobCount = buffer.ReadVariableInt32(); + for (var i = 0; i < jobCount; i++) + { + JobBans.Add(new ProtoId(buffer.ReadString())); + } - for (var i = 0; i < count; i++) + AntagBans.Clear(); + var antagCount = buffer.ReadVariableInt32(); + for (var i = 0; i < antagCount; i++) { - Bans.Add(buffer.ReadString()); + AntagBans.Add(new ProtoId(buffer.ReadString())); } } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.WriteVariableInt32(Bans.Count); + buffer.WriteVariableInt32(JobBans.Count); + foreach (var ban in JobBans) + { + buffer.Write(ban.Id); + } - foreach (var ban in Bans) + buffer.WriteVariableInt32(AntagBans.Count); + foreach (var ban in AntagBans) { - buffer.Write(ban); + buffer.Write(ban.Id); } } } diff --git a/Resources/Locale/en-US/job/role-ban-command.ftl b/Resources/Locale/en-US/job/role-ban-command.ftl index d898ccd48d2..5e7dab48c35 100644 --- a/Resources/Locale/en-US/job/role-ban-command.ftl +++ b/Resources/Locale/en-US/job/role-ban-command.ftl @@ -42,7 +42,6 @@ cmd-roleban-severity-parse = ${severity} is not a valid severity\n{$help}. cmd-roleban-arg-count = Invalid amount of arguments. cmd-roleban-job-parse = Job {$job} does not exist. cmd-roleban-name-parse = Unable to find a player with that name. -cmd-roleban-existing = {$target} already has a role ban for {$role}. cmd-roleban-success = Role banned {$target} from {$role} with reason {$reason} {$length}. cmd-roleban-inf = permanently diff --git a/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/computer_wallmount_apt.png b/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/computer_wallmount_apt.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce14736ed7fcdf38298a30bd192c867af506eaa GIT binary patch literal 1437 zcmV;O1!DS%P)$Z8}S5HW~H<6#96BW48SSrD?| z3Y%mP@fe85{D9;Ta!GcU-Gp6qP**o9<2Z8~`FiG!HPzi!uX}oajQxT1wq0HI-dFWr zzk08RV#J6MBSwsu7Xv2E*JQG_sF=A|EEvb?6;_#)PbdBLPo+|nOeSf|mTk`c+}z(( zsZ^+3F1y+e4Gj@dwibkhZjtXx03$QD(e;1nM1qpZ1dWYta_$!w7if5REtRTO8r0ej zzy2DfQm@id@i(f~WIC-~zqNMw&>?HPUvBkdBGjv*+WMc9T<6zT{g801h7< zZy5kcr_;`iV-=K2+g(C%n;SQNrRC*2l3`rfyZ2q=^V!)y>HF`#rrX7TX?Y|6V-QbE0m{@rb)8dQOaOukTi_8^Q9rQxby9i2N`M+m z-+%O&-rN1DOAuSU9TxB_7cTwbdfRI=7S{iCEgx|A_N+J*#BOjjvzk^`R$R}5Y{MC) z%7E{dI8c20&aU@e+s}oCLXa-R00~j`Wy%M%ou!fP@w1Sv{N;Z-SKTAFVbJZ!lD%9zXY zqLdgiK}T{sjn$aRYctLj#muPFW2BKgJzcU+22fo5$J(#BGzO(P2Zt-}k)4UM24Dk{ zvt0DdITP}hZ$6MG#D+j$0Sr{HxYS7?rEt*=aAq1aAC)ZP6##ZIfZs~!Bv2*BVFXU`)Lhp~~ZB z4$=v+5`Y*0%IPaXCxLVHd~_vhs0A4)r_19tA(E9K@&QnenPug5b0`%4qI~{&J$`a5A6~L=awRhbdAoHzTOGNb4*sqlU1RVJ0s>{oZ1PFEN%)zFR`#(AnMb_N`#y}C4 z0Vn9?^i&e4v3hYl&jc$0e9|J<2!#AdfKYnTG%DcS9|9p1VNr*ip8mnt^Yaxl<9wnG zRv&;4ooGw5F_#TitG5MYue z@<7OIVDc~rjstK+wOjC$B?!bTz-xDC(=Q78G!o!sQVco4Qsrqg(y9j>twBeWzY9z^sE5ga|S`h#GP00000NkvXXu0mjfkgTF- literal 0 HcmV?d00001 diff --git a/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/meta.json b/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/meta.json index 16f0f5e41ff..2c1f15f7af9 100644 --- a/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/meta.json +++ b/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/meta.json @@ -3,7 +3,7 @@ "license": "CC-BY-SA-3.0", "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894, resprited by whatston3 (GitHub)", "extra_copyright": [ - "screen_mass, screen_radar by Tych0thesynth (GitHub), screen_crew, screen_records edited by dustylens (Github)" + "screen_mass, screen_radar by Tych0thesynth (GitHub), screen_crew, screen_records edited by dustylens (Github), apartment screens edited by Rtasva" ], "size": { "x": 32, @@ -33,6 +33,10 @@ { "name": "computer_wallmount", "directions": 4 + }, + { + "name": "computer_wallmount_apt", + "directions": 4 }, { "name": "computer_wallmount_frame", @@ -67,6 +71,50 @@ 0.2 ] ] + }, + { + "name": "screen_occupied", + "directions": 4, + "delays": [ + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ] + ] + }, + { + "name": "screen_vacant", + "directions": 4, + "delays": [ + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ], + [ + 1.2, + 1.2 + ] + ] }, { "name": "screen_crew", diff --git a/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_occupied.png b/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_occupied.png new file mode 100644 index 0000000000000000000000000000000000000000..191c812ec6497c902ed257581a51c884ac348e99 GIT binary patch literal 765 zcmV8nu5Ww+s(V^)U10B0|%+|fx`#fFRA_$T$d7kd^-mS0Dv4B0gn+hf&a=uU!NlBzF z{eMg3FEIQ%(Xx&L00000003YCRxwpvug{x{{VusRexIGocDsuzg1!!S$1W-`2;xPZ0zBWjKJ@G35`5?S(65iDH0L!? z(g0X0y#}(;%}#)WNiotIM3--)o1Fj$lOm%0+W-_)$ng&>-BT_mw&q7t6$guH^QO@fIE-q zP||#O`5r$a^;g8rcAUnyX&s=q zKRKp#z$srCOa2;IY9F9BrS<`e6}m81vTZ^a#!}jb>i`4UO`%G+jolP#vIx+%;Zljs vm}-~b<&-A`KcNo5Pvigq0002sWQfRrDfy~Nxai&300000NkvXXu0mjfA!=xH literal 0 HcmV?d00001 diff --git a/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_vacant.png b/Resources/Textures/_NF/Structures/Machines/computer_wallmount.rsi/screen_vacant.png new file mode 100644 index 0000000000000000000000000000000000000000..d66fd27956b1bbfb3c4d18597474c9348a934894 GIT binary patch literal 788 zcmV+v1MB>WP)tEFwd@}J({KmUC0 zUyr|z)1Q;GTz+3>tFJjU1Nm~gzP8h!>*ULy+2S|Nm)OQR<4;}t_ZH=(zHIE;zXzj4 zb%2mjvl%)>$fy{47Pk5EyIhOF~Da>$3VWv^Wz{rD3*#r1~HiL@%%VQ4GN3==omn*jSRW|!RXJ)X~gxn za_tOgme$o`sf2IHPvQgtJ zLT*3%bf*cq{h8Apt7H~b79Xgui$P!h!x9XU{hR9k~z*`s;_6MBW8N1Z(*9iH8 zj!;6r<(V`!bdn%Htu8>7M-{CO*0k|uP`Rn53rB)3l8Zyld*gFq-5Z~o(76ajbrL!k zAwnn79RS1Mqs!$*mxYx=m&=PP3o{Vi0je3^@j!;(0?Fm!Re7*eLHu(0;)BF5myZ}E z(OE$7T!5vjU=8wv#y3>KW__kBgZaN!X5@^YBMb0*d6k~-7&|1PB5Rx;lhZ)BXYAyZ zVvL=fGC@=jsPq0R4Q;(h_d!^5W|Wi#6wh@vh0X!8QdJi+Ul++s!2?#Z?728zU_9su zC3O<=07m&{4={D$Qi;QuYRjL(UkEDh5&VUqENLLz!N14>00000003Y$h{!+PPH~Hg SmoZHM0000 Date: Wed, 25 Mar 2026 17:46:54 -0600 Subject: [PATCH 5/7] fix --- .../Storage/EntitySystems/SpawnItemsOnUseSystem.cs | 10 ++++++++-- .../Systems/Debris/SimpleFloorPlanPopulatorSystem.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs index 65b1b78e378..931753b4ded 100644 --- a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs +++ b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs @@ -36,7 +36,10 @@ private void CalculatePrice(EntityUid uid, SpawnItemsOnUseComponent component, r foreach (var entry in ungrouped) { - var protUid = Spawn(entry.PrototypeId, MapCoordinates.Nullspace); + if (entry.PrototypeId is not {} prototypeId || !_proto.HasIndex(prototypeId)) + continue; + + var protUid = Spawn(prototypeId, MapCoordinates.Nullspace); // Calculate the average price of the possible spawned items args.Price += _pricing.GetPrice(protUid) * entry.SpawnProbability * entry.GetAmount(getAverage: true); @@ -48,7 +51,10 @@ private void CalculatePrice(EntityUid uid, SpawnItemsOnUseComponent component, r { foreach (var entry in group.Entries) { - var protUid = Spawn(entry.PrototypeId, MapCoordinates.Nullspace); + if (entry.PrototypeId is not {} prototypeId || !_proto.HasIndex(prototypeId)) + continue; + + var protUid = Spawn(prototypeId, MapCoordinates.Nullspace); // Calculate the average price of the possible spawned items args.Price += _pricing.GetPrice(protUid) * diff --git a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs index a575e055ef8..4a954ae54ba 100644 --- a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs +++ b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs @@ -26,11 +26,17 @@ private void OnFloorPlanBuilt(EntityUid uid, SimpleFloorPlanPopulatorComponent c { var placeables = new List(4); var grid = Comp(uid); + var tiles = new List(); var enumerator = _map.GetAllTilesEnumerator(uid, grid); while (enumerator.MoveNext(out var tile)) { - var coords = grid.GridTileToLocal(tile.Value.GridIndices); - var selector = tile.Value.Tile.GetContentTileDefinition(_tileDefinition).ID; + tiles.Add(tile); + } + + foreach (var tile in tiles) + { + var coords = grid.GridTileToLocal(tile.GridIndices); + var selector = tile.Tile.GetContentTileDefinition(_tileDefinition).ID; if (!component.Caches.TryGetValue(selector, out var cache)) continue; From 8d34e0d56d4cf3df446544f02ad0fea4af31aabc Mon Sep 17 00:00:00 2001 From: fenndragon Date: Wed, 25 Mar 2026 17:52:17 -0600 Subject: [PATCH 6/7] fix --- .../Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs index 4a954ae54ba..1ada2726030 100644 --- a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs +++ b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs @@ -30,7 +30,8 @@ private void OnFloorPlanBuilt(EntityUid uid, SimpleFloorPlanPopulatorComponent c var enumerator = _map.GetAllTilesEnumerator(uid, grid); while (enumerator.MoveNext(out var tile)) { - tiles.Add(tile); + if (tile is {} tileRef) + tiles.Add(tileRef); } foreach (var tile in tiles) From c095c5bf8f0231f3dbf8cbbc21b3f6899ded0348 Mon Sep 17 00:00:00 2001 From: TheChech Date: Wed, 25 Mar 2026 23:55:27 -0400 Subject: [PATCH 7/7] killing protos that no work killing protos that no work --- Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml | 4 ++-- Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml index 59b9cd6d327..34f4ba2ddc0 100644 --- a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml +++ b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/engineer.yml @@ -63,7 +63,7 @@ children: - id: RCD - id: RCDAmmo - - id: BoxFolderEngiForms + # - id: BoxFolderEngiForms - type: entity id: HLLockerEngineerFilledHardsuit @@ -102,4 +102,4 @@ - id: RCD - id: RCDAmmo - id: MetalFoamGrenade - - id: BoxFolderEngiForms \ No newline at end of file + # - id: BoxFolderEngiForms diff --git a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml index eda3942bd68..a966e439956 100644 --- a/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/_HL/Catalog/Fills/Lockers/heads.yml @@ -172,7 +172,7 @@ - id: ShipyardRCDAmmo - id: trayScanner - id: CEIDCard - - id: BoxFolderEngiForms + # - id: BoxFolderEngiForms # Hardsuit table, used for suit storage as well - type: entityTable