Stable to master (#42599)

Ban database refactor (#42495)

* Ban DB refactor seems to work at a basic level for PostgreSQL

* New ban creation API

Supports all the new functionality (multiple players/addresses/hwids/roles/rounds per ban).

* Make the migration irreversible

* Re-implement ban notifications

The server ID check is no longer done as admins may want to place bans spanning multiple rounds irrelevant of the source server.

* Fix some split query warnings

* Implement migration on SQLite

* More comments

* Remove required from ban reason

SS14.Admin changes would like this

* More missing AsSplitQuery() calls

* Fix missing ban type filter

* Fix old CreateServerBan API with permanent time

* Fix department and role ban commands with permanent time

* Re-add banhits navigation property

Dropped this on accident, SS14.Admin needs it.

* More ban API fixes.

* Don't fetch ban exemption info for role bans

Not relevant, reduces query performance

* Regenerate migrations

* Fix adminnotes command for players that never connected

Would blow up handling null player records. Not a new bug introduced by the refactor, but I ran into it.

* Great shame... I accidentally committed submodule update...

* Update GDPR scripts

* Fix sandbox violation

* Fix bans with duplicate info causing DB exceptions

Most notably happened with role bans, as multiple departments may include the same role.
This commit is contained in:
Pieter-Jan Briers
2026-01-23 15:34:23 +01:00
committed by GitHub
parent facd7da394
commit 29b7fc4463
65 changed files with 7712 additions and 2698 deletions

View File

@@ -1,4 +1,5 @@
using System.Numerics; using System.Linq;
using System.Numerics;
using Content.Client.Administration.UI.BanList.Bans; using Content.Client.Administration.UI.BanList.Bans;
using Content.Client.Administration.UI.BanList.RoleBans; using Content.Client.Administration.UI.BanList.RoleBans;
using Content.Client.Eui; using Content.Client.Eui;
@@ -73,7 +74,7 @@ public sealed class BanListEui : BaseEui
return date.ToString("MM/dd/yyyy h:mm tt"); return date.ToString("MM/dd/yyyy h:mm tt");
} }
public static void SetData<T>(IBanListLine<T> line, SharedServerBan ban) where T : SharedServerBan public static void SetData<T>(IBanListLine<T> line, SharedBan ban) where T : SharedBan
{ {
line.Reason.Text = ban.Reason; line.Reason.Text = ban.Reason;
line.BanTime.Text = FormatDate(ban.BanTime); line.BanTime.Text = FormatDate(ban.BanTime);
@@ -94,20 +95,20 @@ public sealed class BanListEui : BaseEui
line.BanningAdmin.Text = ban.BanningAdminName; line.BanningAdmin.Text = ban.BanningAdminName;
} }
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedServerBan private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedBan
{ {
_popup?.Close(); _popup?.Close();
_popup = null; _popup = null;
var ban = line.Ban; var ban = line.Ban;
var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value)); 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 ? string.Empty
: Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address)); : Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address))));
var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId)); var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds)));
var guid = ban.UserId == null var guid = ban.UserIds.Length == 0
? string.Empty ? 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); _popup = new BanListIdsPopup(id, ip, hwid, guid);

View File

@@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
} }
public void SetBans(List<SharedServerBan> bans) public void SetBans(List<SharedBan> bans)
{ {
for (var i = Bans.ChildCount - 1; i >= 1; i--) for (var i = Bans.ChildCount - 1; i >= 1; i--)
{ {

View File

@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.BanList.Bans; namespace Content.Client.Administration.UI.BanList.Bans;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedServerBan> public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedBan>
{ {
public SharedServerBan Ban { get; } public SharedBan Ban { get; }
public event Action<BanListLine>? IdsClicked; public event Action<BanListLine>? IdsClicked;
public BanListLine(SharedServerBan ban) public BanListLine(SharedBan ban)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);

View File

@@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls;
namespace Content.Client.Administration.UI.BanList; namespace Content.Client.Administration.UI.BanList;
public interface IBanListLine<T> where T : SharedServerBan public interface IBanListLine<T> where T : SharedBan
{ {
T Ban { get; } T Ban { get; }
Label Reason { get; } Label Reason { get; }

View File

@@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
} }
public void SetRoleBans(List<SharedServerRoleBan> bans) public void SetRoleBans(List<SharedBan> bans)
{ {
for (var i = RoleBans.ChildCount - 1; i >= 1; i--) for (var i = RoleBans.ChildCount - 1; i >= 1; i--)
{ {

View File

@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.BanList.RoleBans; namespace Content.Client.Administration.UI.BanList.RoleBans;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedServerRoleBan> public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedBan>
{ {
public SharedServerRoleBan Ban { get; } public SharedBan Ban { get; }
public event Action<RoleBanListLine>? IdsClicked; public event Action<RoleBanListLine>? IdsClicked;
public RoleBanListLine(SharedServerRoleBan ban) public RoleBanListLine(SharedBan ban)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
@@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedS
IdsHidden.OnPressed += IdsPressed; IdsHidden.OnPressed += IdsPressed;
BanListEui.SetData(this, ban); BanListEui.SetData(this, ban);
Role.Text = ban.Role; Role.Text = string.Join(", ", ban.Roles ?? []);
} }
private void IdsPressed(ButtonEventArgs buttonEventArgs) private void IdsPressed(ButtonEventArgs buttonEventArgs)

View File

@@ -70,7 +70,7 @@ public sealed partial class AdminNotesLine : BoxContainer
TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
ServerLabel.Text = Note.ServerName ?? "Unknown"; 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; AdminLabel.Text = Note.CreatedByName;
PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h"; PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h";
@@ -143,7 +143,12 @@ public sealed partial class AdminNotesLine : BoxContainer
private string FormatRoleBanMessage() private string FormatRoleBanMessage()
{ {
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} "); var rolesText = string.Join(
", ",
// Explicit cast here to avoid sandbox violation.
(IEnumerable<BanRoleDef>?)Note.BannedRoles ?? [new BanRoleDef("what", "You should not be seeing this")]);
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} ");
return FormatBanMessageCommon(banMessage); return FormatBanMessageCommon(banMessage);
} }

View File

@@ -32,9 +32,9 @@ public sealed partial class AdminNotesLinePopup : Popup
IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id)); IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id));
TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType)); TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType));
SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None)); 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-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)); 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"))); 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)); EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName));

View File

@@ -25,8 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
[Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary<string, TimeSpan> _roles = new(); private readonly Dictionary<string, TimeSpan> _roles = new();
private readonly List<string> _jobBans = new(); private readonly List<ProtoId<JobPrototype>> _jobBans = new();
private readonly List<string> _antagBans = new(); private readonly List<ProtoId<AntagPrototype>> _antagBans = new();
private readonly List<string> _jobWhitelists = new(); private readonly List<string> _jobWhitelists = new();
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;

View File

@@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands
// No bans on record // No bans on record
Assert.Multiple(async () => Assert.Multiple(async () =>
{ {
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); Assert.That(await sDatabase.GetBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty);
}); });
// Try to pardon a ban that does not exist // Try to pardon a ban that does not exist
@@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Still no bans on record // Still no bans on record
Assert.Multiple(async () => Assert.Multiple(async () =>
{ {
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); Assert.That(await sDatabase.GetBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty);
}); });
var banReason = "test"; var banReason = "test";
@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Should have one ban on record now // Should have one ban on record now
Assert.Multiple(async () => Assert.Multiple(async () =>
{ {
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); Assert.That(await sDatabase.GetBanAsync(1), 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));
}); });
await pair.RunTicksSync(5); await pair.RunTicksSync(5);
@@ -70,17 +70,17 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2")); await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
// The existing ban is unaffected // 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.Multiple(async () =>
{ {
Assert.That(ban, Is.Not.Null); 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 // Check that it matches
Assert.That(ban.Id, Is.EqualTo(1)); 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.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
Assert.That(ban.ExpirationTime, Is.Not.Null); Assert.That(ban.ExpirationTime, Is.Not.Null);
Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
@@ -95,20 +95,20 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1")); await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
// No bans should be returned // 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 // Direct id lookup returns a pardoned ban
var pardonedBan = await sDatabase.GetServerBanAsync(1); var pardonedBan = await sDatabase.GetBanAsync(1);
Assert.Multiple(async () => Assert.Multiple(async () =>
{ {
// Check that it matches // Check that it matches
Assert.That(pardonedBan, Is.Not.Null); Assert.That(pardonedBan, Is.Not.Null);
// The list is still returned since that ignores pardons // 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.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.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
Assert.That(pardonedBan.ExpirationTime, Is.Not.Null); Assert.That(pardonedBan.ExpirationTime, Is.Not.Null);
Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
@@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.Multiple(async () => Assert.Multiple(async () =>
{ {
// No bans should be returned // 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 // 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 // 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. // Reconnect client. Slightly faster than dirtying the pair.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
using System;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using NpgsqlTypes;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class BanRefactor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ban",
columns: table => new
{
ban_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
type = table.Column<byte>(type: "smallint", nullable: false),
playtime_at_note = table.Column<TimeSpan>(type: "interval", nullable: false),
ban_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
expiration_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
reason = table.Column<string>(type: "text", nullable: false),
severity = table.Column<int>(type: "integer", nullable: false),
banning_admin = table.Column<Guid>(type: "uuid", nullable: true),
last_edited_by_id = table.Column<Guid>(type: "uuid", nullable: true),
last_edited_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
exempt_flags = table.Column<int>(type: "integer", nullable: false),
auto_delete = table.Column<bool>(type: "boolean", nullable: false),
hidden = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban", x => x.ban_id);
table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
table.ForeignKey(
name: "FK_ban_player_banning_admin",
column: x => x.banning_admin,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_ban_player_last_edited_by_id",
column: x => x.last_edited_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "ban_address",
columns: table => new
{
ban_address_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
address = table.Column<NpgsqlInet>(type: "inet", nullable: false),
ban_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_address", x => x.ban_address_id);
table.CheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
table.ForeignKey(
name: "FK_ban_address_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_hwid",
columns: table => new
{
ban_hwid_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
hwid = table.Column<byte[]>(type: "bytea", nullable: false),
hwid_type = table.Column<int>(type: "integer", nullable: false),
ban_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id);
table.ForeignKey(
name: "FK_ban_hwid_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_player",
columns: table => new
{
ban_player_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
ban_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_player", x => x.ban_player_id);
table.ForeignKey(
name: "FK_ban_player_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_role",
columns: table => new
{
ban_role_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
role_type = table.Column<string>(type: "text", nullable: false),
role_id = table.Column<string>(type: "text", nullable: false),
ban_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_role", x => x.ban_role_id);
table.ForeignKey(
name: "FK_ban_role_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_round",
columns: table => new
{
ban_round_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ban_id = table.Column<int>(type: "integer", nullable: false),
round_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_round", x => x.ban_round_id);
table.ForeignKey(
name: "FK_ban_round_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ban_round_round_round_id",
column: x => x.round_id,
principalTable: "round",
principalColumn: "round_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "unban",
columns: table => new
{
unban_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ban_id = table.Column<int>(type: "integer", nullable: false),
unbanning_admin = table.Column<Guid>(type: "uuid", nullable: true),
unban_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_unban", x => x.unban_id);
table.ForeignKey(
name: "FK_unban_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ban_banning_admin",
table: "ban",
column: "banning_admin");
migrationBuilder.CreateIndex(
name: "IX_ban_last_edited_by_id",
table: "ban",
column: "last_edited_by_id");
migrationBuilder.CreateIndex(
name: "IX_ban_address_ban_id",
table: "ban_address",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_hwid_ban_id",
table: "ban_hwid",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_player_ban_id",
table: "ban_player",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_player_user_id_ban_id",
table: "ban_player",
columns: new[] { "user_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ban_role_ban_id",
table: "ban_role",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_role_role_type_role_id_ban_id",
table: "ban_role",
columns: new[] { "role_type", "role_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ban_round_ban_id",
table: "ban_round",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_round_round_id_ban_id",
table: "ban_round",
columns: new[] { "round_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_unban_ban_id",
table: "unban",
column: "ban_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_server_ban_hit_ban_ban_id",
table: "server_ban_hit",
column: "ban_id",
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.Sql("""
CREATE INDEX "IX_ban_address_address"
ON ban_address
USING gist
(address inet_ops)
INCLUDE (ban_id);
CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id"
ON ban_hwid
(hwid_type, hwid, ban_id);
CREATE UNIQUE INDEX "IX_ban_address_address_ban_id"
ON ban_address
(address, ban_id);
""");
migrationBuilder.Sql($"""
-- REMOVE:
-- TRUNCATE ban RESTART IDENTITY CASCADE;
--
-- Insert game bans
--
INSERT INTO
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
SELECT
server_ban_id, {(int)BanType.Server}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden
FROM
server_ban;
-- Update ID sequence to be after newly inserted IDs.
SELECT setval('ban_ban_id_seq', (SELECT MAX(ban_id) FROM ban));
-- Insert ban player records.
INSERT INTO
ban_player (user_id, ban_id)
SELECT
player_user_id, server_ban_id
FROM
server_ban
WHERE
player_user_id IS NOT NULL;
-- Insert ban address records.
INSERT INTO
ban_address (address, ban_id)
SELECT
address, server_ban_id
FROM
server_ban
WHERE
address IS NOT NULL;
-- Insert ban HWID records.
INSERT INTO
ban_hwid (hwid, hwid_type, ban_id)
SELECT
hwid, hwid_type, server_ban_id
FROM
server_ban
WHERE
hwid IS NOT NULL;
-- Insert ban unban records.
INSERT INTO
unban (ban_id, unbanning_admin, unban_time)
SELECT
ban_id, unbanning_admin, unban_time
FROM server_unban;
-- Insert ban round records.
INSERT INTO
ban_round (round_id, ban_id)
SELECT
round_id, server_ban_id
FROM
server_ban
WHERE
round_id IS NOT NULL;
--
-- Insert role bans
-- This shit is a pain in the ass
-- > Declarative language
-- > Has to write procedural code in it
--
-- Create mapping table from role ban -> server ban.
-- We have to manually calculate the new ban IDs by using the sequence.
-- We also want to merge role ban records because the game code previously did that in some UI,
-- and that code is now gone, expecting the DB to do it.
-- Create a table to store IDs to merge.
CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE) ON COMMIT DROP;
-- TRUNCATE _role_ban_import_merge_map;
-- Create a table to store merged IDs -> new ban IDs
CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE) ON COMMIT DROP;
-- TRUNCATE _role_ban_import_id_map;
-- Calculate merged role bans.
INSERT INTO
_role_ban_import_merge_map
SELECT
(
SELECT
sub.server_role_ban_id
FROM
server_role_ban AS sub
LEFT JOIN server_role_unban AS sub_unban
ON sub_unban.ban_id = sub.server_role_ban_id
WHERE
main.reason IS NOT DISTINCT FROM sub.reason
AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id
AND main.address IS NOT DISTINCT FROM sub.address
AND main.hwid IS NOT DISTINCT FROM sub.hwid
AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type
AND date_trunc('second', main.ban_time, 'utc') = date_trunc('second', sub.ban_time, 'utc')
AND (
(main.expiration_time IS NULL) = (sub.expiration_time IS NULL)
OR date_trunc('minute', main.expiration_time, 'utc') = date_trunc('minute', sub.expiration_time, 'utc')
)
AND main.round_id IS NOT DISTINCT FROM sub.round_id
AND main.severity IS NOT DISTINCT FROM sub.severity
AND main.hidden IS NOT DISTINCT FROM sub.hidden
AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin
AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL)
ORDER BY
sub.server_role_ban_id ASC
LIMIT 1
), main.server_role_ban_id
FROM
server_role_ban AS main
LEFT JOIN server_role_unban AS main_unban
ON main_unban.ban_id = main.server_role_ban_id;
-- Assign new ban IDs for merged IDs.
INSERT INTO
_role_ban_import_id_map
SELECT
DISTINCT ON (merge_id)
nextval('ban_ban_id_seq'),
merge_id
FROM
_role_ban_import_merge_map;
-- I sure fucking wish CTEs could span multiple queries...
-- Insert new ban records
INSERT INTO
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
SELECT
im.ban_id, {(int)BanType.Role}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id;
-- Insert role ban player records.
INSERT INTO
ban_player (user_id, ban_id)
SELECT
player_user_id, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND player_user_id IS NOT NULL;
-- Insert role ban address records.
INSERT INTO
ban_address (address, ban_id)
SELECT
address, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND address IS NOT NULL;
-- Insert role ban HWID records.
INSERT INTO
ban_hwid (hwid, hwid_type, ban_id)
SELECT
hwid, hwid_type, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND hwid IS NOT NULL;
-- Insert role ban role records.
INSERT INTO
ban_role (role_type, role_id, ban_id)
SELECT
split_part(role_id, ':', 1), split_part(role_id, ':', 2), im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = mm.server_role_ban_id
-- Yes, we have some messy ban records which, after merging, end up with duplicate roles.
ON CONFLICT DO NOTHING;
-- Insert role unban records.
INSERT INTO
unban (ban_id, unbanning_admin, unban_time)
SELECT
im.ban_id, unbanning_admin, unban_time
FROM server_role_unban sru
INNER JOIN _role_ban_import_id_map im
ON im.merge_id = sru.ban_id;
-- Insert role rounds
INSERT INTO
ban_round (round_id, ban_id)
SELECT
round_id, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND round_id IS NOT NULL;
""");
migrationBuilder.DropForeignKey(
name: "FK_server_ban_hit_server_ban_ban_id",
table: "server_ban_hit");
migrationBuilder.DropTable(
name: "server_role_unban");
migrationBuilder.DropTable(
name: "server_unban");
migrationBuilder.DropTable(
name: "server_role_ban");
migrationBuilder.DropTable(
name: "server_ban");
migrationBuilder.Sql($"""
CREATE OR REPLACE FUNCTION send_server_ban_notification()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'ban_notification',
json_build_object('ban_id', NEW.ban_id)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER notify_on_server_ban_insert
AFTER INSERT ON ban
FOR EACH ROW
WHEN (NEW.type = {(int)BanType.Server})
EXECUTE FUNCTION send_server_ban_notification();
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new NotSupportedException("This migration cannot be rolled back");
}
}
}

View File

@@ -519,6 +519,221 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null); b.ToTable("assigned_user_id", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.Ban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoDelete")
.HasColumnType("boolean")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("uuid")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("integer")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("uuid")
.HasColumnName("last_edited_by_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("interval")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text")
.HasColumnName("reason");
b.Property<int>("Severity")
.HasColumnType("integer")
.HasColumnName("severity");
b.Property<byte>("Type")
.HasColumnType("smallint")
.HasColumnName("type");
b.HasKey("Id")
.HasName("PK_ban");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.ToTable("ban", null, t =>
{
t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
});
});
modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_address_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<NpgsqlInet>("Address")
.HasColumnType("inet")
.HasColumnName("address");
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.HasKey("Id")
.HasName("PK_ban_address");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_address_ban_id");
b.ToTable("ban_address", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
});
});
modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_hwid_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.HasKey("Id")
.HasName("PK_ban_hwid");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_hwid_ban_id");
b.ToTable("ban_hwid", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_player_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("PK_ban_player");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_player_ban_id");
b.HasIndex("UserId", "BanId")
.IsUnique();
b.ToTable("ban_player", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_role_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_id");
b.Property<string>("RoleType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_type");
b.HasKey("Id")
.HasName("PK_ban_role");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_role_ban_id");
b.HasIndex("RoleType", "RoleId", "BanId")
.IsUnique();
b.ToTable("ban_role", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanRound", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_round_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<int>("RoundId")
.HasColumnType("integer")
.HasColumnName("round_id");
b.HasKey("Id")
.HasName("PK_ban_round");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_round_ban_id");
b.HasIndex("RoundId", "BanId")
.IsUnique();
b.ToTable("ban_round", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b => modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1069,95 +1284,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("server", (string)null); b.ToTable("server", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("server_ban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<NpgsqlInet?>("Address")
.HasColumnType("inet")
.HasColumnName("address");
b.Property<bool>("AutoDelete")
.HasColumnType("boolean")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("uuid")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("integer")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("uuid")
.HasColumnName("last_edited_by_id");
b.Property<Guid?>("PlayerUserId")
.HasColumnType("uuid")
.HasColumnName("player_user_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("interval")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text")
.HasColumnName("reason");
b.Property<int?>("RoundId")
.HasColumnType("integer")
.HasColumnName("round_id");
b.Property<int>("Severity")
.HasColumnType("integer")
.HasColumnName("severity");
b.HasKey("Id")
.HasName("PK_server_ban");
b.HasIndex("Address");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_server_ban_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_server_ban_round_id");
b.ToTable("server_ban", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
{ {
b.Property<Guid>("UserId") b.Property<Guid>("UserId")
@@ -1207,152 +1333,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("server_ban_hit", (string)null); b.ToTable("server_ban_hit", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("server_role_ban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<NpgsqlInet?>("Address")
.HasColumnType("inet")
.HasColumnName("address");
b.Property<DateTime>("BanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("uuid")
.HasColumnName("banning_admin");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("uuid")
.HasColumnName("last_edited_by_id");
b.Property<Guid?>("PlayerUserId")
.HasColumnType("uuid")
.HasColumnName("player_user_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("interval")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text")
.HasColumnName("reason");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_id");
b.Property<int?>("RoundId")
.HasColumnType("integer")
.HasColumnName("round_id");
b.Property<int>("Severity")
.HasColumnType("integer")
.HasColumnName("severity");
b.HasKey("Id")
.HasName("PK_server_role_ban");
b.HasIndex("Address");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_server_role_ban_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_server_role_ban_round_id");
b.ToTable("server_role_ban", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("role_unban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("uuid")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_server_role_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_role_unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("unban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("uuid")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_server_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Trait", b => modelBuilder.Entity("Content.Server.Database.Trait", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1380,6 +1360,36 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("trait", (string)null); b.ToTable("trait", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.Unban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("unban_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BanId")
.HasColumnType("integer")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("uuid")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1664,6 +1674,123 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile"); b.Navigation("Profile");
}); });
modelBuilder.Entity("Content.Server.Database.Ban", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_ban_player_last_edited_by_id");
b.Navigation("CreatedBy");
b.Navigation("LastEditedBy");
});
modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Addresses")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_address_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Hwids")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_hwid_ban_ban_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("BanHwidId")
.HasColumnType("integer")
.HasColumnName("ban_hwid_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("hwid_type");
b1.HasKey("BanHwidId");
b1.ToTable("ban_hwid");
b1.WithOwner()
.HasForeignKey("BanHwidId")
.HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id");
});
b.Navigation("Ban");
b.Navigation("HWId")
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Players")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_player_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanRole", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Roles")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_role_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanRound", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Rounds")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_round_ban_ban_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_round_round_round_id");
b.Navigation("Ban");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{ {
b.HasOne("Content.Server.Database.Server", "Server") b.HasOne("Content.Server.Database.Server", "Server")
@@ -1820,70 +1947,14 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Server"); b.Navigation("Server");
}); });
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_ban_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("integer")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
{ {
b.HasOne("Content.Server.Database.ServerBan", "Ban") b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("BanHits") .WithMany("BanHits")
.HasForeignKey("BanId") .HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired() .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") b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
.WithMany("BanHits") .WithMany("BanHits")
@@ -1897,86 +1968,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Connection"); b.Navigation("Connection");
}); });
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerRoleBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_role_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerRoleBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("integer")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
{
b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
{
b.HasOne("Content.Server.Database.ServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_server_unban_server_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.Trait", b => modelBuilder.Entity("Content.Server.Database.Trait", b =>
{ {
b.HasOne("Content.Server.Database.Profile", "Profile") b.HasOne("Content.Server.Database.Profile", "Profile")
@@ -1989,6 +1980,18 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile"); 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 => modelBuilder.Entity("PlayerRound", b =>
{ {
b.HasOne("Content.Server.Database.Player", null) b.HasOne("Content.Server.Database.Player", null)
@@ -2023,6 +2026,23 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Flags"); 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 => modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{ {
b.Navigation("BanHits"); b.Navigation("BanHits");
@@ -2052,10 +2072,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("AdminServerBansLastEdited"); b.Navigation("AdminServerBansLastEdited");
b.Navigation("AdminServerRoleBansCreated");
b.Navigation("AdminServerRoleBansLastEdited");
b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsCreated");
b.Navigation("AdminWatchlistsDeleted"); b.Navigation("AdminWatchlistsDeleted");
@@ -2104,18 +2120,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Rounds"); 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 #pragma warning restore 612, 618
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,498 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class BanRefactor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ban",
columns: table => new
{
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
type = table.Column<byte>(type: "INTEGER", nullable: false),
playtime_at_note = table.Column<TimeSpan>(type: "TEXT", nullable: false),
ban_time = table.Column<DateTime>(type: "TEXT", nullable: false),
expiration_time = table.Column<DateTime>(type: "TEXT", nullable: true),
reason = table.Column<string>(type: "TEXT", nullable: false),
severity = table.Column<int>(type: "INTEGER", nullable: false),
banning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
last_edited_by_id = table.Column<Guid>(type: "TEXT", nullable: true),
last_edited_at = table.Column<DateTime>(type: "TEXT", nullable: true),
exempt_flags = table.Column<int>(type: "INTEGER", nullable: false),
auto_delete = table.Column<bool>(type: "INTEGER", nullable: false),
hidden = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban", x => x.ban_id);
table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
table.ForeignKey(
name: "FK_ban_player_banning_admin",
column: x => x.banning_admin,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_ban_player_last_edited_by_id",
column: x => x.last_edited_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "ban_address",
columns: table => new
{
ban_address_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
address = table.Column<string>(type: "TEXT", nullable: false),
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_address", x => x.ban_address_id);
table.ForeignKey(
name: "FK_ban_address_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_hwid",
columns: table => new
{
ban_hwid_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
hwid = table.Column<byte[]>(type: "BLOB", nullable: false),
hwid_type = table.Column<int>(type: "INTEGER", nullable: false),
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id);
table.ForeignKey(
name: "FK_ban_hwid_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_player",
columns: table => new
{
ban_player_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_player", x => x.ban_player_id);
table.ForeignKey(
name: "FK_ban_player_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_role",
columns: table => new
{
ban_role_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
role_type = table.Column<string>(type: "TEXT", nullable: false),
role_id = table.Column<string>(type: "TEXT", nullable: false),
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_role", x => x.ban_role_id);
table.ForeignKey(
name: "FK_ban_role_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ban_round",
columns: table => new
{
ban_round_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ban_id = table.Column<int>(type: "INTEGER", nullable: false),
round_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_round", x => x.ban_round_id);
table.ForeignKey(
name: "FK_ban_round_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ban_round_round_round_id",
column: x => x.round_id,
principalTable: "round",
principalColumn: "round_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "unban",
columns: table => new
{
unban_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ban_id = table.Column<int>(type: "INTEGER", nullable: false),
unbanning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
unban_time = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_unban", x => x.unban_id);
table.ForeignKey(
name: "FK_unban_ban_ban_id",
column: x => x.ban_id,
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ban_banning_admin",
table: "ban",
column: "banning_admin");
migrationBuilder.CreateIndex(
name: "IX_ban_last_edited_by_id",
table: "ban",
column: "last_edited_by_id");
migrationBuilder.CreateIndex(
name: "IX_ban_address_ban_id",
table: "ban_address",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_hwid_ban_id",
table: "ban_hwid",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_player_ban_id",
table: "ban_player",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_player_user_id_ban_id",
table: "ban_player",
columns: new[] { "user_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ban_role_ban_id",
table: "ban_role",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_role_role_type_role_id_ban_id",
table: "ban_role",
columns: new[] { "role_type", "role_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ban_round_ban_id",
table: "ban_round",
column: "ban_id");
migrationBuilder.CreateIndex(
name: "IX_ban_round_round_id_ban_id",
table: "ban_round",
columns: new[] { "round_id", "ban_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_unban_ban_id",
table: "unban",
column: "ban_id",
unique: true);
migrationBuilder.Sql("""
CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id"
ON ban_hwid
(hwid_type, hwid, ban_id);
CREATE UNIQUE INDEX "IX_ban_address_address_ban_id"
ON ban_address
(address, ban_id);
""");
migrationBuilder.Sql("""
--
-- Insert game bans
--
INSERT INTO
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
SELECT
server_ban_id, 0, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden
FROM
server_ban;
-- Insert ban player records.
INSERT INTO
ban_player (user_id, ban_id)
SELECT
player_user_id, server_ban_id
FROM
server_ban
WHERE
player_user_id IS NOT NULL;
-- Insert ban address records.
INSERT INTO
ban_address (address, ban_id)
SELECT
address, server_ban_id
FROM
server_ban
WHERE
address IS NOT NULL;
-- Insert ban HWID records.
INSERT INTO
ban_hwid (hwid, hwid_type, ban_id)
SELECT
hwid, hwid_type, server_ban_id
FROM
server_ban
WHERE
hwid IS NOT NULL;
-- Insert ban unban records.
INSERT INTO
unban (ban_id, unbanning_admin, unban_time)
SELECT
ban_id, unbanning_admin, unban_time
FROM server_unban;
-- Insert ban round records.
INSERT INTO
ban_round (round_id, ban_id)
SELECT
round_id, server_ban_id
FROM
server_ban
WHERE
round_id IS NOT NULL;
--
-- Insert role bans
-- This shit is a pain in the ass
-- > Declarative language
-- > Has to write procedural code in it
--
-- Create mapping table from role ban -> server ban.
-- We have to manually calculate the new ban IDs by using the sequence.
-- We also want to merge role ban records because the game code previously did that in some UI,
-- and that code is now gone, expecting the DB to do it.
-- Create a table to store IDs to merge.
CREATE TEMPORARY TABLE _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE);
-- Create a table to store merged IDs -> new ban IDs
CREATE TEMPORARY TABLE _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE);
-- Calculate merged role bans.
INSERT INTO
_role_ban_import_merge_map
SELECT
(
SELECT
sub.server_role_ban_id
FROM
server_role_ban AS sub
LEFT JOIN server_role_unban AS sub_unban
ON sub_unban.ban_id = sub.server_role_ban_id
WHERE
main.reason IS NOT DISTINCT FROM sub.reason
AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id
AND main.address IS NOT DISTINCT FROM sub.address
AND main.hwid IS NOT DISTINCT FROM sub.hwid
AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type
AND main.ban_time = sub.ban_time
AND (
(main.expiration_time IS NULL) = (sub.expiration_time IS NULL)
OR main.expiration_time = sub.expiration_time
)
AND main.round_id IS NOT DISTINCT FROM sub.round_id
AND main.severity IS NOT DISTINCT FROM sub.severity
AND main.hidden IS NOT DISTINCT FROM sub.hidden
AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin
AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL)
ORDER BY
sub.server_role_ban_id ASC
LIMIT 1
), main.server_role_ban_id
FROM
server_role_ban AS main
LEFT JOIN server_role_unban AS main_unban
ON main_unban.ban_id = main.server_role_ban_id;
-- Assign new ban IDs for merged IDs.
INSERT OR IGNORE INTO
_role_ban_import_id_map
SELECT
merge_id + (SELECT seq FROM sqlite_sequence WHERE name = 'ban'),
merge_id
FROM
_role_ban_import_merge_map;
-- I sure fucking wish CTEs could span multiple queries...
-- Insert new ban records
INSERT INTO
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
SELECT
im.ban_id, 1, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id;
-- Insert role ban player records.
INSERT INTO
ban_player (user_id, ban_id)
SELECT
player_user_id, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND player_user_id IS NOT NULL;
-- Insert role ban address records.
INSERT INTO
ban_address (address, ban_id)
SELECT
address, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND address IS NOT NULL;
-- Insert role ban HWID records.
INSERT INTO
ban_hwid (hwid, hwid_type, ban_id)
SELECT
hwid, hwid_type, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND hwid IS NOT NULL;
-- Insert role ban role records.
INSERT INTO
ban_role (role_type, role_id, ban_id)
SELECT
substr(role_id, 1, instr(role_id, ':')-1),
substr(role_id, instr(role_id, ':')+1),
im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = mm.server_role_ban_id
-- Yes, we have some messy ban records which, after merging, end up with duplicate roles.
ON CONFLICT DO NOTHING;
-- Insert role unban records.
INSERT INTO
unban (ban_id, unbanning_admin, unban_time)
SELECT
im.ban_id, unbanning_admin, unban_time
FROM server_role_unban sru
INNER JOIN _role_ban_import_id_map im
ON im.merge_id = sru.ban_id;
-- Insert role rounds
INSERT INTO
ban_round (round_id, ban_id)
SELECT
round_id, im.ban_id
FROM
_role_ban_import_id_map im
INNER JOIN _role_ban_import_merge_map mm
ON im.merge_id = mm.merge_id
INNER JOIN server_role_ban srb
ON srb.server_role_ban_id = im.merge_id
WHERE mm.merge_id = mm.server_role_ban_id
AND round_id IS NOT NULL;
""");
migrationBuilder.AddForeignKey(
name: "FK_server_ban_hit_ban_ban_id",
table: "server_ban_hit",
column: "ban_id",
principalTable: "ban",
principalColumn: "ban_id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.DropForeignKey(
name: "FK_server_ban_hit_server_ban_ban_id",
table: "server_ban_hit");
migrationBuilder.DropTable(
name: "server_role_unban");
migrationBuilder.DropTable(
name: "server_unban");
migrationBuilder.DropTable(
name: "server_role_ban");
migrationBuilder.DropTable(
name: "server_ban");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new NotSupportedException("This migration cannot be rolled back");
}
}
}

View File

@@ -489,6 +489,207 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null); b.ToTable("assigned_user_id", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.Ban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("TEXT")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("INTEGER")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("TEXT")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("TEXT")
.HasColumnName("last_edited_by_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("TEXT")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("reason");
b.Property<int>("Severity")
.HasColumnType("INTEGER")
.HasColumnName("severity");
b.Property<byte>("Type")
.HasColumnType("INTEGER")
.HasColumnName("type");
b.HasKey("Id")
.HasName("PK_ban");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.ToTable("ban", null, t =>
{
t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
});
});
modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_address_id");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("address");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.HasKey("Id")
.HasName("PK_ban_address");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_address_ban_id");
b.ToTable("ban_address", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_hwid_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.HasKey("Id")
.HasName("PK_ban_hwid");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_hwid_ban_id");
b.ToTable("ban_hwid", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_player_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("PK_ban_player");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_player_ban_id");
b.HasIndex("UserId", "BanId")
.IsUnique();
b.ToTable("ban_player", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_role_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_id");
b.Property<string>("RoleType")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_type");
b.HasKey("Id")
.HasName("PK_ban_role");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_role_ban_id");
b.HasIndex("RoleType", "RoleId", "BanId")
.IsUnique();
b.ToTable("ban_role", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanRound", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_round_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<int>("RoundId")
.HasColumnType("INTEGER")
.HasColumnName("round_id");
b.HasKey("Id")
.HasName("PK_ban_round");
b.HasIndex("BanId")
.HasDatabaseName("IX_ban_round_ban_id");
b.HasIndex("RoundId", "BanId")
.IsUnique();
b.ToTable("ban_round", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b => modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1010,91 +1211,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("server", (string)null); b.ToTable("server", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("server_ban_id");
b.Property<string>("Address")
.HasColumnType("TEXT")
.HasColumnName("address");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("TEXT")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("INTEGER")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("TEXT")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("TEXT")
.HasColumnName("last_edited_by_id");
b.Property<Guid?>("PlayerUserId")
.HasColumnType("TEXT")
.HasColumnName("player_user_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("TEXT")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("reason");
b.Property<int?>("RoundId")
.HasColumnType("INTEGER")
.HasColumnName("round_id");
b.Property<int>("Severity")
.HasColumnType("INTEGER")
.HasColumnName("severity");
b.HasKey("Id")
.HasName("PK_server_ban");
b.HasIndex("Address");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_server_ban_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_server_ban_round_id");
b.ToTable("server_ban", null, t =>
{
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
{ {
b.Property<Guid>("UserId") b.Property<Guid>("UserId")
@@ -1142,144 +1258,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("server_ban_hit", (string)null); b.ToTable("server_ban_hit", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("server_role_ban_id");
b.Property<string>("Address")
.HasColumnType("TEXT")
.HasColumnName("address");
b.Property<DateTime>("BanTime")
.HasColumnType("TEXT")
.HasColumnName("ban_time");
b.Property<Guid?>("BanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("banning_admin");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
b.Property<DateTime?>("LastEditedAt")
.HasColumnType("TEXT")
.HasColumnName("last_edited_at");
b.Property<Guid?>("LastEditedById")
.HasColumnType("TEXT")
.HasColumnName("last_edited_by_id");
b.Property<Guid?>("PlayerUserId")
.HasColumnType("TEXT")
.HasColumnName("player_user_id");
b.Property<TimeSpan>("PlaytimeAtNote")
.HasColumnType("TEXT")
.HasColumnName("playtime_at_note");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("reason");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_id");
b.Property<int?>("RoundId")
.HasColumnType("INTEGER")
.HasColumnName("round_id");
b.Property<int>("Severity")
.HasColumnType("INTEGER")
.HasColumnName("severity");
b.HasKey("Id")
.HasName("PK_server_role_ban");
b.HasIndex("Address");
b.HasIndex("BanningAdmin");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_server_role_ban_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_server_role_ban_round_id");
b.ToTable("server_role_ban", null, t =>
{
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("role_unban_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("TEXT")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_server_role_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_role_unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("unban_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("TEXT")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_server_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Trait", b => modelBuilder.Entity("Content.Server.Database.Trait", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1305,6 +1283,34 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("trait", (string)null); b.ToTable("trait", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.Unban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("unban_id");
b.Property<int>("BanId")
.HasColumnType("INTEGER")
.HasColumnName("ban_id");
b.Property<DateTime>("UnbanTime")
.HasColumnType("TEXT")
.HasColumnName("unban_time");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnType("TEXT")
.HasColumnName("unbanning_admin");
b.HasKey("Id")
.HasName("PK_unban");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("unban", (string)null);
});
modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1587,6 +1593,123 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile"); b.Navigation("Profile");
}); });
modelBuilder.Entity("Content.Server.Database.Ban", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_ban_player_last_edited_by_id");
b.Navigation("CreatedBy");
b.Navigation("LastEditedBy");
});
modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Addresses")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_address_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Hwids")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_hwid_ban_ban_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("BanHwidId")
.HasColumnType("INTEGER")
.HasColumnName("ban_hwid_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.HasColumnType("INTEGER")
.HasColumnName("hwid_type");
b1.HasKey("BanHwidId");
b1.ToTable("ban_hwid");
b1.WithOwner()
.HasForeignKey("BanHwidId")
.HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id");
});
b.Navigation("Ban");
b.Navigation("HWId")
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Players")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_player_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanRole", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Roles")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_role_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.BanRound", b =>
{
b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("Rounds")
.HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_round_ban_ban_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_ban_round_round_round_id");
b.Navigation("Ban");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{ {
b.HasOne("Content.Server.Database.Server", "Server") b.HasOne("Content.Server.Database.Server", "Server")
@@ -1743,70 +1866,14 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Server"); b.Navigation("Server");
}); });
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_ban_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
{ {
b.HasOne("Content.Server.Database.ServerBan", "Ban") b.HasOne("Content.Server.Database.Ban", "Ban")
.WithMany("BanHits") .WithMany("BanHits")
.HasForeignKey("BanId") .HasForeignKey("BanId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired() .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") b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
.WithMany("BanHits") .WithMany("BanHits")
@@ -1820,86 +1887,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Connection"); b.Navigation("Connection");
}); });
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminServerRoleBansCreated")
.HasForeignKey("BanningAdmin")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_role_ban_player_banning_admin");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminServerRoleBansLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
{
b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
{
b.HasOne("Content.Server.Database.ServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_server_unban_server_ban_ban_id");
b.Navigation("Ban");
});
modelBuilder.Entity("Content.Server.Database.Trait", b => modelBuilder.Entity("Content.Server.Database.Trait", b =>
{ {
b.HasOne("Content.Server.Database.Profile", "Profile") b.HasOne("Content.Server.Database.Profile", "Profile")
@@ -1912,6 +1899,18 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile"); 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 => modelBuilder.Entity("PlayerRound", b =>
{ {
b.HasOne("Content.Server.Database.Player", null) b.HasOne("Content.Server.Database.Player", null)
@@ -1946,6 +1945,23 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Flags"); 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 => modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{ {
b.Navigation("BanHits"); b.Navigation("BanHits");
@@ -1975,10 +1991,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("AdminServerBansLastEdited"); b.Navigation("AdminServerBansLastEdited");
b.Navigation("AdminServerRoleBansCreated");
b.Navigation("AdminServerRoleBansLastEdited");
b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsCreated");
b.Navigation("AdminWatchlistsDeleted"); b.Navigation("AdminWatchlistsDeleted");
@@ -2027,18 +2039,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Rounds"); 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 #pragma warning restore 612, 618
} }
} }

View File

@@ -0,0 +1,328 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using NpgsqlTypes;
// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength
namespace Content.Server.Database;
//
// Contains model definitions primarily related to bans.
//
internal static class ModelBan
{
public static void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Ban>()
.HasOne(b => b.CreatedBy)
.WithMany(pl => pl.AdminServerBansCreated)
.HasForeignKey(b => b.BanningAdmin)
.HasPrincipalKey(pl => pl.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Ban>()
.HasOne(b => b.LastEditedBy)
.WithMany(pl => pl.AdminServerBansLastEdited)
.HasForeignKey(b => b.LastEditedById)
.HasPrincipalKey(pl => pl.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<BanPlayer>()
.HasIndex(bp => new { bp.UserId, bp.BanId })
.IsUnique();
modelBuilder.Entity<BanHwid>()
.OwnsOne(bp => bp.HWId)
.Property(hwid => hwid.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<BanRole>()
.HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId })
.IsUnique();
modelBuilder.Entity<BanRound>()
.HasIndex(bp => new { bp.RoundId, bp.BanId })
.IsUnique();
// Following indices have to be made manually by migration, due to limitations in EF Core:
// https://github.com/dotnet/efcore/issues/11336
// https://github.com/npgsql/efcore.pg/issues/2567
// modelBuilder.Entity<BanAddress>()
// .HasIndex(bp => new { bp.Address, bp.BanId })
// .IsUnique();
// modelBuilder.Entity<BanHwid>()
// .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid })
// .IsUnique();
// (postgres only)
// modelBuilder.Entity<BanAddress>()
// .HasIndex(ba => ba.Address)
// .IncludeProperties(ba => ba.BanId)
// .IsUnique()
// .HasMethod("gist")
// .HasOperators("inet_ops");
modelBuilder.Entity<Ban>()
.ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0"));
}
}
/// <summary>
/// Specifies a ban of some kind.
/// </summary>
/// <remarks>
/// <para>
/// Bans come in two types: <see cref="BanType.Server"/> and <see cref="BanType.Role"/>,
/// distinguished with <see cref="Type"/>.
/// </para>
/// <para>
/// Bans have one or more "matching data", these being <see cref="BanAddress"/>, <see cref="BanPlayer"/>,
/// and <see cref="BanHwid"/> entities. If a player's connection info matches any of these,
/// the ban's effects will apply to that player.
/// </para>
/// <para>
/// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually
/// ("unbanned") by an admin, which is stored as an <see cref="Unban"/> entity existing for this ban.
/// </para>
/// </remarks>
public sealed class Ban
{
public int Id { get; set; }
/// <summary>
/// Whether this is a role or server ban.
/// </summary>
public required BanType Type { get; set; }
public TimeSpan PlaytimeAtNote { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
/// </summary>
public DateTime BanTime { get; set; }
/// <summary>
/// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
/// </summary>
public DateTime? ExpirationTime { get; set; }
/// <summary>
/// The administrator-stated reason for applying the ban.
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
/// The severity of the incident
/// </summary>
public NoteSeverity Severity { get; set; }
/// <summary>
/// User ID of the admin that initially applied the ban.
/// </summary>
[ForeignKey(nameof(CreatedBy))]
public Guid? BanningAdmin { get; set; }
public Player? CreatedBy { get; set; }
/// <summary>
/// User ID of the admin that last edited the note
/// </summary>
[ForeignKey(nameof(LastEditedBy))]
public Guid? LastEditedById { get; set; }
public Player? LastEditedBy { get; set; }
public DateTime? LastEditedAt { get; set; }
/// <summary>
/// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
/// </summary>
public ServerBanExemptFlags ExemptFlags { get; set; }
/// <summary>
/// Whether this ban should be automatically deleted from the database when it expires.
/// </summary>
/// <remarks>
/// This isn't done automatically by the game,
/// you will need to set up something like a cron job to clear this from your database,
/// using a command like this:
/// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time &lt; NOW()"
/// </remarks>
public bool AutoDelete { get; set; }
/// <summary>
/// Whether to display this ban in the admin remarks (notes) panel
/// </summary>
public bool Hidden { get; set; }
/// <summary>
/// If present, an administrator has manually repealed this ban.
/// </summary>
public Unban? Unban { get; set; }
public List<BanRound>? Rounds { get; set; }
public List<BanPlayer>? Players { get; set; }
public List<BanAddress>? Addresses { get; set; }
public List<BanHwid>? Hwids { get; set; }
public List<BanRole>? Roles { get; set; }
public List<ServerBanHit>? BanHits { get; set; }
}
/// <summary>
/// Base type for entities that specify ban matching data.
/// </summary>
public interface IBanSelector
{
int BanId { get; }
Ban? Ban { get; }
}
/// <summary>
/// Indicates that a ban was related to a round (e.g. placed on that round).
/// </summary>
public sealed class BanRound
{
public int Id { get; set; }
/// <summary>
/// The ID of the ban to which this round was relevant.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
public Ban? Ban { get; set; }
/// <summary>
/// The ID of the round to which this ban was relevant to.
/// </summary>
[ForeignKey(nameof(Round))]
public int RoundId { get; set; }
public Round? Round { get; set; }
}
/// <summary>
/// Specifies a player that a <see cref="T:Database.Ban"/> matches.
/// </summary>
public sealed class BanPlayer : IBanSelector
{
public int Id { get; set; }
/// <summary>
/// The user ID of the banned player.
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// The ID of the ban to which this applies.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
public Ban? Ban { get; set; }
}
/// <summary>
/// Specifies an IP address range that a <see cref="T:Database.Ban"/> matches.
/// </summary>
public sealed class BanAddress : IBanSelector
{
public int Id { get; set; }
/// <summary>
/// The address range being matched.
/// </summary>
public required NpgsqlInet Address { get; set; }
/// <summary>
/// The ID of the ban to which this applies.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
public Ban? Ban { get; set; }
}
/// <summary>
/// Specifies a HWID that a <see cref="T:Database.Ban"/> matches.
/// </summary>
public sealed class BanHwid : IBanSelector
{
public int Id { get; set; }
/// <summary>
/// The HWID being matched.
/// </summary>
public required TypedHwid HWId { get; set; }
/// <summary>
/// The ID of the ban to which this applies.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
public Ban? Ban { get; set; }
}
/// <summary>
/// A single role banned among a greater role ban record.
/// </summary>
/// <remarks>
/// <see cref="Ban"/>s of type <see cref="BanType.Role"/> should have one or more <see cref="BanRole"/>s
/// to store which roles are actually banned.
/// It is invalid for <see cref="BanType.Server"/> bans to have <see cref="BanRole"/> entities.
/// </remarks>
public sealed class BanRole
{
public int Id { get; set; }
/// <summary>
/// What type of role is being banned. For example <c>Job</c> or <c>Antag</c>.
/// </summary>
public required string RoleType { get; set; }
/// <summary>
/// The ID of the role being banned. This is probably something like a prototype.
/// </summary>
public required string RoleId { get; set; }
/// <summary>
/// The ID of the ban to which this applies.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
public Ban? Ban { get; set; }
}
/// <summary>
/// An explicit repeal of a <see cref="Ban"/> by an administrator.
/// Having an entry for a ban neutralizes it.
/// </summary>
public sealed class Unban
{
public int Id { get; set; }
/// <summary>
/// The ID of ban that is being repealed.
/// </summary>
[ForeignKey(nameof(Ban))]
public int BanId { get; set; }
/// <summary>
/// The ban that is being repealed.
/// </summary>
public Ban? Ban { get; set; }
/// <summary>
/// The admin that repealed the ban.
/// </summary>
public Guid? UnbanningAdmin { get; set; }
/// <summary>
/// The time the ban was repealed.
/// </summary>
public DateTime UnbanTime { get; set; }
}

View File

@@ -9,7 +9,6 @@ using System.Net;
using System.Text.Json; using System.Text.Json;
using Content.Shared.Database; using Content.Shared.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NpgsqlTypes;
namespace Content.Server.Database namespace Content.Server.Database
{ {
@@ -31,13 +30,17 @@ namespace Content.Server.Database
public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!; public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!;
public DbSet<Whitelist> Whitelist { get; set; } = null!; public DbSet<Whitelist> Whitelist { get; set; } = null!;
public DbSet<Blacklist> Blacklist { get; set; } = null!; public DbSet<Blacklist> Blacklist { get; set; } = null!;
public DbSet<ServerBan> Ban { get; set; } = default!; public DbSet<Ban> Ban { get; set; } = default!;
public DbSet<ServerUnban> Unban { get; set; } = default!; public DbSet<BanRound> BanRound { get; set; } = default!;
public DbSet<BanPlayer> BanPlayer { get; set; } = default!;
public DbSet<BanAddress> BanAddress { get; set; } = default!;
public DbSet<BanHwid> BanHwid { get; set; } = default!;
public DbSet<BanRole> BanRole { get; set; } = default!;
public DbSet<Unban> Unban { get; set; } = default!;
public DbSet<ServerBanExemption> BanExemption { get; set; } = default!; public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!; public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!;
public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!; public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
public DbSet<PlayTime> PlayTime { get; set; } = default!; public DbSet<PlayTime> PlayTime { get; set; } = default!;
public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!; public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
public DbSet<AdminNote> AdminNotes { get; set; } = null!; public DbSet<AdminNote> AdminNotes { get; set; } = null!;
@@ -145,43 +148,11 @@ namespace Content.Server.Database
modelBuilder.Entity<AdminLogPlayer>() modelBuilder.Entity<AdminLogPlayer>()
.HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId}); .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
modelBuilder.Entity<ServerBan>()
.HasIndex(p => p.PlayerUserId);
modelBuilder.Entity<ServerBan>()
.HasIndex(p => p.Address);
modelBuilder.Entity<ServerBan>()
.HasIndex(p => p.PlayerUserId);
modelBuilder.Entity<ServerUnban>()
.HasIndex(p => p.BanId)
.IsUnique();
modelBuilder.Entity<ServerBan>().ToTable(t =>
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
// Ban exemption can't have flags 0 since that wouldn't exempt anything. // Ban exemption can't have flags 0 since that wouldn't exempt anything.
// The row should be removed if setting to 0. // The row should be removed if setting to 0.
modelBuilder.Entity<ServerBanExemption>().ToTable(t => modelBuilder.Entity<ServerBanExemption>().ToTable(t =>
t.HasCheckConstraint("FlagsNotZero", "flags != 0")); t.HasCheckConstraint("FlagsNotZero", "flags != 0"));
modelBuilder.Entity<ServerRoleBan>()
.HasIndex(p => p.PlayerUserId);
modelBuilder.Entity<ServerRoleBan>()
.HasIndex(p => p.Address);
modelBuilder.Entity<ServerRoleBan>()
.HasIndex(p => p.PlayerUserId);
modelBuilder.Entity<ServerRoleUnban>()
.HasIndex(p => p.BanId)
.IsUnique();
modelBuilder.Entity<ServerRoleBan>().ToTable(t =>
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
modelBuilder.Entity<Player>() modelBuilder.Entity<Player>()
.HasIndex(p => p.UserId) .HasIndex(p => p.UserId)
.IsUnique(); .IsUnique();
@@ -296,34 +267,6 @@ namespace Content.Server.Database
t.HasCheckConstraint("NotDismissedAndSeen", t.HasCheckConstraint("NotDismissedAndSeen",
"NOT dismissed OR seen")); "NOT dismissed OR seen"));
modelBuilder.Entity<ServerBan>()
.HasOne(ban => ban.CreatedBy)
.WithMany(author => author.AdminServerBansCreated)
.HasForeignKey(ban => ban.BanningAdmin)
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<ServerBan>()
.HasOne(ban => ban.LastEditedBy)
.WithMany(author => author.AdminServerBansLastEdited)
.HasForeignKey(ban => ban.LastEditedById)
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<ServerRoleBan>()
.HasOne(ban => ban.CreatedBy)
.WithMany(author => author.AdminServerRoleBansCreated)
.HasForeignKey(ban => ban.BanningAdmin)
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<ServerRoleBan>()
.HasOne(ban => ban.LastEditedBy)
.WithMany(author => author.AdminServerRoleBansLastEdited)
.HasForeignKey(ban => ban.LastEditedById)
.HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<RoleWhitelist>() modelBuilder.Entity<RoleWhitelist>()
.HasOne(w => w.Player) .HasOne(w => w.Player)
.WithMany(p => p.JobWhitelists) .WithMany(p => p.JobWhitelists)
@@ -342,26 +285,6 @@ namespace Content.Server.Database
.Property(p => p.Type) .Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy); .HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ConnectionLog>() modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId) .OwnsOne(p => p.HWId)
.Property(p => p.Hwid) .Property(p => p.Hwid)
@@ -371,6 +294,8 @@ namespace Content.Server.Database
.OwnsOne(p => p.HWId) .OwnsOne(p => p.HWId)
.Property(p => p.Type) .Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy); .HasDefaultValue(HwidType.Legacy);
ModelBan.OnModelCreating(modelBuilder);
} }
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText) public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -591,10 +516,8 @@ namespace Content.Server.Database
public List<AdminMessage> AdminMessagesCreated { get; set; } = null!; public List<AdminMessage> AdminMessagesCreated { get; set; } = null!;
public List<AdminMessage> AdminMessagesLastEdited { get; set; } = null!; public List<AdminMessage> AdminMessagesLastEdited { get; set; } = null!;
public List<AdminMessage> AdminMessagesDeleted { get; set; } = null!; public List<AdminMessage> AdminMessagesDeleted { get; set; } = null!;
public List<ServerBan> AdminServerBansCreated { get; set; } = null!; public List<Ban> AdminServerBansCreated { get; set; } = null!;
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!; public List<Ban> AdminServerBansLastEdited { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
public List<RoleWhitelist> JobWhitelists { get; set; } = null!; public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
} }
@@ -724,30 +647,6 @@ namespace Content.Server.Database
[ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!; [ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!;
} }
// Used by SS14.Admin
public interface IBanCommon<TUnban> where TUnban : IUnbanCommon
{
int Id { get; set; }
Guid? PlayerUserId { get; set; }
NpgsqlInet? Address { get; set; }
TypedHwid? HWId { get; set; }
DateTime BanTime { get; set; }
DateTime? ExpirationTime { get; set; }
string Reason { get; set; }
NoteSeverity Severity { get; set; }
Guid? BanningAdmin { get; set; }
TUnban? Unban { get; set; }
}
// Used by SS14.Admin
public interface IUnbanCommon
{
int Id { get; set; }
int BanId { get; set; }
Guid? UnbanningAdmin { get; set; }
DateTime UnbanTime { get; set; }
}
/// <summary> /// <summary>
/// Flags for use with <see cref="ServerBanExemption"/>. /// Flags for use with <see cref="ServerBanExemption"/>.
/// </summary> /// </summary>
@@ -785,138 +684,6 @@ namespace Content.Server.Database
// @formatter:on // @formatter:on
} }
/// <summary>
/// A ban from playing on the server.
/// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server.
/// </summary>
/// <remarks>
/// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing).
/// </remarks>
[Table("server_ban"), Index(nameof(PlayerUserId))]
public class ServerBan : IBanCommon<ServerUnban>
{
public int Id { get; set; }
[ForeignKey("Round")]
public int? RoundId { get; set; }
public Round? Round { get; set; }
/// <summary>
/// The user ID of the banned player.
/// </summary>
public Guid? PlayerUserId { get; set; }
[Required] public TimeSpan PlaytimeAtNote { get; set; }
/// <summary>
/// CIDR IP address range of the ban. The whole range can match the ban.
/// </summary>
public NpgsqlInet? Address { get; set; }
/// <summary>
/// Hardware ID of the banned player.
/// </summary>
public TypedHwid? HWId { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
/// </summary>
public DateTime BanTime { get; set; }
/// <summary>
/// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
/// </summary>
public DateTime? ExpirationTime { get; set; }
/// <summary>
/// The administrator-stated reason for applying the ban.
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
/// The severity of the incident
/// </summary>
public NoteSeverity Severity { get; set; }
/// <summary>
/// User ID of the admin that applied the ban.
/// </summary>
[ForeignKey("CreatedBy")]
public Guid? BanningAdmin { get; set; }
public Player? CreatedBy { get; set; }
/// <summary>
/// User ID of the admin that last edited the note
/// </summary>
[ForeignKey("LastEditedBy")]
public Guid? LastEditedById { get; set; }
public Player? LastEditedBy { get; set; }
/// <summary>
/// When the ban was last edited
/// </summary>
public DateTime? LastEditedAt { get; set; }
/// <summary>
/// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
/// </summary>
public ServerBanExemptFlags ExemptFlags { get; set; }
/// <summary>
/// If present, an administrator has manually repealed this ban.
/// </summary>
public ServerUnban? Unban { get; set; }
/// <summary>
/// Whether this ban should be automatically deleted from the database when it expires.
/// </summary>
/// <remarks>
/// This isn't done automatically by the game,
/// you will need to set up something like a cron job to clear this from your database,
/// using a command like this:
/// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time &lt; NOW()"
/// </remarks>
public bool AutoDelete { get; set; }
/// <summary>
/// Whether to display this ban in the admin remarks (notes) panel
/// </summary>
public bool Hidden { get; set; }
public List<ServerBanHit> BanHits { get; set; } = null!;
}
/// <summary>
/// An explicit repeal of a <see cref="ServerBan"/> by an administrator.
/// Having an entry for a ban neutralizes it.
/// </summary>
[Table("server_unban")]
public class ServerUnban : IUnbanCommon
{
[Column("unban_id")] public int Id { get; set; }
/// <summary>
/// The ID of ban that is being repealed.
/// </summary>
public int BanId { get; set; }
/// <summary>
/// The ban that is being repealed.
/// </summary>
public ServerBan Ban { get; set; } = null!;
/// <summary>
/// The admin that repealed the ban.
/// </summary>
public Guid? UnbanningAdmin { get; set; }
/// <summary>
/// The time the ban repealed.
/// </summary>
public DateTime UnbanTime { get; set; }
}
/// <summary> /// <summary>
/// An exemption for a specific user to a certain type of <see cref="ServerBan"/>. /// An exemption for a specific user to a certain type of <see cref="ServerBan"/>.
/// </summary> /// </summary>
@@ -937,7 +704,7 @@ namespace Content.Server.Database
/// <summary> /// <summary>
/// The ban flags to exempt this player from. /// The ban flags to exempt this player from.
/// If any bit overlaps <see cref="ServerBan.ExemptFlags"/>, the ban is ignored. /// If any bit overlaps <see cref="Ban.ExemptFlags"/>, the ban is ignored.
/// </summary> /// </summary>
public ServerBanExemptFlags Flags { get; set; } public ServerBanExemptFlags Flags { get; set; }
} }
@@ -1000,54 +767,10 @@ namespace Content.Server.Database
public int BanId { get; set; } public int BanId { get; set; }
public int ConnectionId { 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!; public ConnectionLog Connection { get; set; } = null!;
} }
[Table("server_role_ban"), Index(nameof(PlayerUserId))]
public sealed class ServerRoleBan : IBanCommon<ServerRoleUnban>
{
public int Id { get; set; }
public int? RoundId { get; set; }
public Round? Round { get; set; }
public Guid? PlayerUserId { get; set; }
[Required] public TimeSpan PlaytimeAtNote { get; set; }
public NpgsqlInet? Address { get; set; }
public TypedHwid? HWId { get; set; }
public DateTime BanTime { get; set; }
public DateTime? ExpirationTime { get; set; }
public string Reason { get; set; } = null!;
public NoteSeverity Severity { get; set; }
[ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; }
public Player? CreatedBy { get; set; }
[ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
public Player? LastEditedBy { get; set; }
public DateTime? LastEditedAt { get; set; }
public ServerRoleUnban? Unban { get; set; }
public bool Hidden { get; set; }
public string RoleId { get; set; } = null!;
}
[Table("server_role_unban")]
public sealed class ServerRoleUnban : IUnbanCommon
{
[Column("role_unban_id")] public int Id { get; set; }
public int BanId { get; set; }
public ServerRoleBan Ban { get; set; } = null!;
public Guid? UnbanningAdmin { get; set; }
public DateTime UnbanTime { get; set; }
}
[Table("play_time")] [Table("play_time")]
public sealed class PlayTime public sealed class PlayTime
{ {
@@ -1247,31 +970,31 @@ namespace Content.Server.Database
/// <summary> /// <summary>
/// The reason for the ban. /// The reason for the ban.
/// </summary> /// </summary>
/// <seealso cref="ServerBan.Reason"/> /// <seealso cref="Ban.Reason"/>
public string Reason { get; set; } = ""; public string Reason { get; set; } = "";
/// <summary> /// <summary>
/// Exemptions granted to the ban. /// Exemptions granted to the ban.
/// </summary> /// </summary>
/// <seealso cref="ServerBan.ExemptFlags"/> /// <seealso cref="Ban.ExemptFlags"/>
public ServerBanExemptFlags ExemptFlags { get; set; } public ServerBanExemptFlags ExemptFlags { get; set; }
/// <summary> /// <summary>
/// Severity of the ban /// Severity of the ban
/// </summary> /// </summary>
/// <seealso cref="ServerBan.Severity"/> /// <seealso cref="Ban.Severity"/>
public NoteSeverity Severity { get; set; } public NoteSeverity Severity { get; set; }
/// <summary> /// <summary>
/// Ban will be automatically deleted once expired. /// Ban will be automatically deleted once expired.
/// </summary> /// </summary>
/// <seealso cref="ServerBan.AutoDelete"/> /// <seealso cref="Ban.AutoDelete"/>
public bool AutoDelete { get; set; } public bool AutoDelete { get; set; }
/// <summary> /// <summary>
/// Ban is not visible to players in the remarks menu. /// Ban is not visible to players in the remarks menu.
/// </summary> /// </summary>
/// <seealso cref="ServerBan.Hidden"/> /// <seealso cref="Ban.Hidden"/>
public bool Hidden { get; set; } public bool Hidden { get; set; }
} }

View File

@@ -39,10 +39,7 @@ namespace Content.Server.Database
// ReSharper disable StringLiteralTypo // ReSharper disable StringLiteralTypo
// Enforce that an address cannot be IPv6-mapped IPv4. // Enforce that an address cannot be IPv6-mapped IPv4.
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
modelBuilder.Entity<ServerBan>().ToTable(t => modelBuilder.Entity<BanAddress>().ToTable(t =>
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
modelBuilder.Entity<ServerRoleBan>().ToTable( t =>
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
modelBuilder.Entity<Player>().ToTable(t => modelBuilder.Entity<Player>().ToTable(t =>

View File

@@ -58,13 +58,7 @@ namespace Content.Server.Database
); );
modelBuilder modelBuilder
.Entity<ServerBan>() .Entity<BanAddress>()
.Property(e => e.Address)
.HasColumnType("TEXT")
.HasConversion(ipMaskConverter);
modelBuilder
.Entity<ServerRoleBan>()
.Property(e => e.Address) .Property(e => e.Address)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasConversion(ipMaskConverter); .HasConversion(ipMaskConverter);

View File

@@ -1,9 +1,12 @@
using System.Threading.Tasks; using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Administration.BanList; using Content.Shared.Administration.BanList;
using Content.Shared.Database;
using Content.Shared.Eui; using Content.Shared.Eui;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -22,8 +25,8 @@ public sealed class BanListEui : BaseEui
private Guid BanListPlayer { get; set; } private Guid BanListPlayer { get; set; }
private string BanListPlayerName { get; set; } = string.Empty; private string BanListPlayerName { get; set; } = string.Empty;
private List<SharedServerBan> Bans { get; } = new(); private List<SharedBan> Bans { get; } = new();
private List<SharedServerRoleBan> RoleBans { get; } = new(); private List<SharedBan> RoleBans { get; } = new();
public override void Opened() public override void Opened()
{ {
@@ -54,74 +57,38 @@ public sealed class BanListEui : BaseEui
private async Task LoadBans(NetUserId userId) private async Task LoadBans(NetUserId userId)
{ {
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null)) await LoadBansCore(userId, BanType.Server, Bans);
{ await LoadBansCore(userId, BanType.Role, RoleBans);
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
));
}
} }
private async Task LoadRoleBans(NetUserId userId) private async Task LoadBansCore(NetUserId userId, BanType banType, List<SharedBan> list)
{ {
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null)) foreach (var ban in await _db.GetBansAsync(null, userId, null, null, type: banType))
{ {
SharedServerUnban? unban = null; SharedUnban? unban = null;
if (ban.Unban is { } unbanDef) if (ban.Unban is { } unbanDef)
{ {
var unbanningAdmin = unbanDef.UnbanningAdmin == null var unbanningAdmin = unbanDef.UnbanningAdmin == null
? null ? null
: (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; : (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); ImmutableArray<(string, int cidrMask)> ips = [("*Hidden*", 0)];
var hwid = "*Hidden*"; ImmutableArray<string> hwids = ["*Hidden*"];
if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
{ {
ip = ban.Address is { } address ips = [..ban.Addresses.Select(a => (a.address.ToString(), a.cidrMask))];
? (address.address.ToString(), address.cidrMask) hwids = [..ban.HWIds.Select(h => h.ToString())];
: null;
hwid = ban.HWId?.ToString();
} }
RoleBans.Add(new SharedServerRoleBan(
list.Add(new SharedBan(
ban.Id, ban.Id,
ban.UserId, ban.Type,
ip, ban.UserIds,
hwid, ips,
hwids,
ban.BanTime.UtcDateTime, ban.BanTime.UtcDateTime,
ban.ExpirationTime?.UtcDateTime, ban.ExpirationTime?.UtcDateTime,
ban.Reason, ban.Reason,
@@ -129,7 +96,7 @@ public sealed class BanListEui : BaseEui
? null ? null
: (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username,
unban, unban,
ban.Role ban.Roles
)); ));
} }
} }
@@ -144,7 +111,6 @@ public sealed class BanListEui : BaseEui
string.Empty; string.Empty;
await LoadBans(userId); await LoadBans(userId);
await LoadRoleBans(userId);
StateDirty(); StateDirty();
} }

View File

@@ -26,8 +26,8 @@ public sealed class BanPanelEui : BaseEui
private string PlayerName { get; set; } = string.Empty; private string PlayerName { get; set; } = string.Empty;
private IPAddress? LastAddress { get; set; } private IPAddress? LastAddress { get; set; }
private ImmutableTypedHwid? LastHwid { get; set; } private ImmutableTypedHwid? LastHwid { get; set; }
private const int Ipv4_CIDR = 32; private const int Ipv4_CIDR = CreateBanInfo.DefaultMaskIpv4;
private const int Ipv6_CIDR = 64; private const int Ipv6_CIDR = CreateBanInfo.DefaultMaskIpv6;
public BanPanelEui() public BanPanelEui()
{ {
@@ -73,6 +73,15 @@ public sealed class BanPanelEui : BaseEui
return; return;
} }
var isRoleBan = ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0;
CreateBanInfo banInfo = isRoleBan ? new CreateRoleBanInfo(ban.Reason) : new CreateServerBanInfo(ban.Reason);
banInfo.WithBanningAdmin(Player.UserId);
banInfo.WithSeverity(ban.Severity);
if (ban.BanDurationMinutes > 0)
banInfo.WithMinutes(ban.BanDurationMinutes);
(IPAddress, int)? addressRange = null; (IPAddress, int)? addressRange = null;
if (ban.IpAddress is not null) if (ban.IpAddress is not null)
{ {
@@ -113,69 +122,46 @@ public sealed class BanPanelEui : BaseEui
targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid; targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid;
} }
if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0) if (addressRange != null)
banInfo.AddAddressRange(addressRange.Value);
if (targetUid != null)
banInfo.AddUser(targetUid.Value, ban.Target!);
banInfo.AddHWId(targetHWid);
if (isRoleBan)
{ {
var now = DateTimeOffset.UtcNow; var roleBanInfo = (CreateRoleBanInfo)banInfo;
foreach (var role in ban.BannedJobs ?? []) foreach (var row in ban.BannedJobs ?? [])
{ {
_banManager.CreateRoleBan( roleBanInfo.AddJob(row);
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
} }
foreach (var role in ban.BannedAntags ?? []) foreach (var row in ban.BannedAntags ?? [])
{ {
_banManager.CreateRoleBan( roleBanInfo.AddAntag(row);
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
} }
Close(); _banManager.CreateRoleBan(roleBanInfo);
return;
} }
else
if (ban.Erase && targetUid is not null)
{ {
try if (ban.Erase && targetUid is not null)
{ {
if (_entities.TrySystem(out AdminSystem? adminSystem)) try
adminSystem.Erase(targetUid.Value); {
if (_entities.TrySystem(out AdminSystem? adminSystem))
adminSystem.Erase(targetUid.Value);
}
catch (Exception e)
{
_sawmill.Error($"Error while erasing banned player:\n{e}");
}
} }
catch (Exception e)
{
_sawmill.Error($"Error while erasing banned player:\n{e}");
}
}
_banManager.CreateServerBan( _banManager.CreateServerBan((CreateServerBanInfo)banInfo);
targetUid, }
ban.Target,
Player.UserId,
addressRange,
targetHWid,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason
);
Close(); Close();
} }

View File

@@ -90,7 +90,15 @@ public sealed class BanCommand : LocalizedCommands
var targetUid = located.UserId; var targetUid = located.UserId;
var targetHWid = located.LastHWId; 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) public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -39,7 +39,7 @@ public sealed class BanListCommand : LocalizedCommands
if (shell.Player is not { } player) 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) if (bans.Count == 0)
{ {

View File

@@ -96,13 +96,20 @@ public sealed class DepartmentBanCommand : IConsoleCommand
var targetUid = located.UserId; var targetUid = located.UserId;
var targetHWid = located.LastHWId; 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. var banInfo = new CreateRoleBanInfo(reason);
// Without it the note list will get needlessly cluttered. if (minutes > 0)
var now = DateTimeOffset.UtcNow; 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) 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) public CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -3,6 +3,7 @@ using Content.Server.Administration.Notes;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Network;
namespace Content.Server.Administration.Commands; namespace Content.Server.Administration.Commands;
@@ -46,7 +47,7 @@ public sealed class OpenAdminNotesCommand : LocalizedCommands
return; return;
} }
await _adminNotes.OpenEui(player, notedPlayer); await _adminNotes.OpenEui(player, new NetUserId(notedPlayer));
} }
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -27,7 +27,7 @@ namespace Content.Server.Administration.Commands
return; return;
} }
var ban = await _dbManager.GetServerBanAsync(banId); var ban = await _dbManager.GetBanAsync(banId);
if (ban == null) if (ban == null)
{ {
@@ -50,7 +50,7 @@ namespace Content.Server.Administration.Commands
return; return;
} }
await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now)); await _dbManager.AddUnbanAsync(new UnbanDef(banId, player?.UserId, DateTimeOffset.Now));
shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId))); shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId)));
} }

View File

@@ -1,6 +1,4 @@
using System.Linq; using Content.Server.Administration.Managers;
using System.Text;
using Content.Server.Administration.Managers;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Database; using Content.Shared.Database;
@@ -99,12 +97,29 @@ public sealed class RoleBanCommand : IConsoleCommand
var targetUid = located.UserId; var targetUid = located.UserId;
var targetHWid = located.LastHWId; var targetHWid = located.LastHWId;
var banInfo = new CreateRoleBanInfo(reason);
if (minutes > 0)
banInfo.WithMinutes(minutes);
banInfo.AddUser(targetUid, located.Username);
banInfo.WithBanningAdmin(shell.Player?.UserId);
banInfo.AddHWId(targetHWid);
banInfo.WithSeverity(severity);
if (_proto.HasIndex<JobPrototype>(role)) if (_proto.HasIndex<JobPrototype>(role))
_bans.CreateRoleBan<JobPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); {
banInfo.AddJob(new ProtoId<JobPrototype>(role));
}
else if (_proto.HasIndex<AntagPrototype>(role)) else if (_proto.HasIndex<AntagPrototype>(role))
_bans.CreateRoleBan<AntagPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); {
banInfo.AddAntag(new ProtoId<AntagPrototype>(role));
}
else else
{
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role))); shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role)));
return;
}
_bans.CreateRoleBan(banInfo);
} }
public CompletionResult GetCompletion(IConsoleShell shell, string[] args) public CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -1,10 +1,8 @@
using System.Linq; using Content.Server.Administration.BanList;
using System.Text;
using Content.Server.Administration.BanList;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Content.Shared.Database;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Server.Administration.Commands; namespace Content.Server.Administration.Commands;
@@ -48,7 +46,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
if (shell.Player is not { } player) 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) if (bans.Count == 0)
{ {
@@ -58,7 +56,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
foreach (var ban in bans) 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); shell.WriteLine(msg);
} }
return; return;

View File

@@ -41,14 +41,8 @@ public sealed partial class BanManager
private async void ProcessBanNotification(BanNotificationData data) 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}"); _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) if (ban == null)
{ {
_sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?"); _sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?");
@@ -86,15 +80,5 @@ public sealed partial class BanManager
/// </summary> /// </summary>
[JsonRequired, JsonPropertyName("ban_id")] [JsonRequired, JsonPropertyName("ban_id")]
public int BanId { get; init; } public int BanId { get; init; }
/// <summary>
/// The id of the server the ban was made on.
/// This is used to avoid double work checking the ban on the originating server.
/// </summary>
/// <remarks>
/// This is optional in case the ban was made outside a server (SS14.Admin)
/// </remarks>
[JsonPropertyName("server_id")]
public int? ServerId { get; init; }
} }
} }

View File

@@ -1,6 +1,5 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -21,6 +20,7 @@ using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Managers; namespace Content.Server.Administration.Managers;
@@ -43,10 +43,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
public const string SawmillId = "admin.bans"; public const string SawmillId = "admin.bans";
public const string PrefixAntag = "Antag:"; public const string DbTypeAntag = "Antag";
public const string PrefixJob = "Job:"; public const string DbTypeJob = "Job";
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new(); private readonly Dictionary<ICommonSession, List<BanDef>> _cachedRoleBans = new();
// Cached ban exemption flags are used to handle // Cached ban exemption flags are used to handle
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new(); private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
@@ -72,9 +72,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var netChannel = player.Channel; var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId; ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var modernHwids = netChannel.UserData.ModernHWIds; var modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false); var roleBans = await _db.GetBansAsync(
netChannel.RemoteEndPoint.Address,
player.UserId,
hwId,
modernHwids,
false,
type: BanType.Role);
var userRoleBans = new List<ServerRoleBanDef>(); var userRoleBans = new List<BanDef>();
foreach (var ban in roleBans) foreach (var ban in roleBans)
{ {
userRoleBans.Add(ban); userRoleBans.Add(ban);
@@ -115,43 +121,37 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
} }
#region Server Bans #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; var (banDef, expires) = await CreateBanDef(banInfo, BanType.Server, null);
if (minutes > 0)
await _db.AddBanAsync(banDef);
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules))
{ {
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); // Reset their last read rules. They probably need a refresher!
foreach (var (userId, _) in banInfo.Users)
{
await _db.SetLastReadRules(userId, null);
}
} }
_systems.TryGetEntitySystem<GameTicker>(out var ticker); var adminName = banInfo.BanningAdmin == null
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
? Loc.GetString("system-user") ? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); : (await _db.GetPlayerRecordByUserId(banInfo.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
var targetName = target is null ? "null" : $"{targetUsername} ({target})";
var addressRangeString = addressRange != null var targetName = banInfo.Users.Count == 0
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}" ? "null"
: "null"; : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})"));
var hwidString = hwid?.ToString() ?? "null";
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 expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii"; var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
@@ -159,12 +159,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var logMessage = Loc.GetString( var logMessage = Loc.GetString(
key, key,
("admin", adminName), ("admin", adminName),
("severity", severity), ("severity", banDef.Severity),
("expires", expiresString), ("expires", expiresString),
("name", targetName), ("name", targetName),
("ip", addressRangeString), ("ip", addressRangeString),
("hwid", hwidString), ("hwid", hwidString),
("reason", reason)); ("reason", banInfo.Reason));
_sawmill.Info(logMessage); _sawmill.Info(logMessage);
_chat.SendAdminAlert(logMessage); _chat.SendAdminAlert(logMessage);
@@ -172,7 +172,19 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
KickMatchingConnectedPlayers(banDef, "newly placed ban"); KickMatchingConnectedPlayers(banDef, "newly placed ban");
} }
private void KickMatchingConnectedPlayers(ServerBanDef def, string source) private NoteSeverity GetSeverityForServerBan(CreateBanInfo banInfo, CVarDef<string> defaultCVar)
{
if (banInfo.Severity != null)
return banInfo.Severity.Value;
if (Enum.TryParse(_cfg.GetCVar(defaultCVar), true, out NoteSeverity parsedSeverity))
return parsedSeverity;
_sawmill.Error($"CVar {defaultCVar.Name} has invalid ban severity!");
return NoteSeverity.None;
}
private void KickMatchingConnectedPlayers(BanDef def, string source)
{ {
foreach (var player in _playerManager.Sessions) foreach (var player in _playerManager.Sessions)
{ {
@@ -184,7 +196,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
} }
} }
private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) private bool BanMatchesPlayer(ICommonSession player, BanDef ban)
{ {
var playerInfo = new BanMatcher.PlayerInfo var playerInfo = new BanMatcher.PlayerInfo
{ {
@@ -201,7 +213,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
return BanMatcher.BanMatches(ban, playerInfo); 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); var message = def.FormatBanMessage(_cfg, _localizationManager);
player.Channel.Disconnect(message); player.Channel.Disconnect(message);
@@ -211,108 +223,154 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#region Role Bans #region Role Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. public async void CreateRoleBan(CreateRoleBanInfo banInfo)
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan<T>(
NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
ProtoId<T> role,
uint? minutes,
NoteSeverity severity,
string reason,
DateTimeOffset timeOfBan
) where T : class, IPrototype
{ {
string encodedRole; ImmutableArray<BanRoleDef> roleDefs =
[
.. ToBanRoleDef(banInfo.JobPrototypes),
.. ToBanRoleDef(banInfo.AntagPrototypes),
];
// TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced if (roleDefs.Length == 0)
// this check has consciously avoided refactoring Job and Antag prototype. throw new ArgumentException("Must specify at least one role to ban!");
// Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash.
//TODO remove this check as part of the above refactor var (banDef, expires) = await CreateBanDef(banInfo, BanType.Role, roleDefs);
if (_prototypeManager.HasIndex<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(role))
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)
{ {
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype."); if (_playerManager.TryGetSessionById(userId, out var session))
SendRoleBans(session);
return;
} }
// Don't trust the input: make sure the job or antag actually exists.
if (_prototypeManager.HasIndex<JobPrototype>(role))
encodedRole = PrefixJob + role;
else if (_prototypeManager.HasIndex<AntagPrototype>(role))
encodedRole = PrefixAntag + role;
else
{
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
return;
}
DateTimeOffset? expires = null;
if (minutes > 0)
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
_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(
null,
target,
addressRange,
hwid,
timeOfBan,
expires,
roundId,
playtime,
reason,
severity,
banningAdmin,
null,
encodedRole);
if (!await AddRoleBan(banDef))
{
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
return;
}
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 (target is not null && _playerManager.TryGetSessionById(target.Value, out var session))
SendRoleBans(session);
} }
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef) private async Task<(BanDef Ban, DateTimeOffset? Expires)> CreateBanDef(
CreateBanInfo banInfo,
BanType type,
ImmutableArray<BanRoleDef>? roleBans)
{ {
banDef = await _db.AddServerRoleBanAsync(banDef); if (banInfo.Users.Count == 0 && banInfo.HWIds.Count == 0 && banInfo.AddressRanges.Count == 0)
throw new ArgumentException("Must specify at least one user, HWID, or address range");
if (banDef.UserId != null DateTimeOffset? expires = null;
&& _playerManager.TryGetSessionById(banDef.UserId, out var player) if (banInfo.Duration is { } duration)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans)) expires = DateTimeOffset.Now + duration;
ImmutableArray<int> roundIds;
if (banInfo.RoundIds.Count > 0)
{ {
cachedBans.Add(banDef); roundIds = [..banInfo.RoundIds];
}
else if (_systems.TryGetEntitySystem<GameTicker>(out var ticker) && ticker.RoundId != 0)
{
roundIds = [ticker.RoundId];
}
else
{
roundIds = [];
} }
return true; return (new BanDef(
null,
type,
[..banInfo.Users.Select(u => u.UserId)],
[..banInfo.AddressRanges],
[..banInfo.HWIds],
DateTimeOffset.Now,
expires,
roundIds,
await GetPlayTime(banInfo),
banInfo.Reason,
GetSeverityForServerBan(banInfo, CCVars.ServerBanDefaultSeverity),
banInfo.BanningAdmin,
null,
roles: roleBans), expires);
}
private async Task<TimeSpan> GetPlayTime(CreateBanInfo banInfo)
{
var firstPlayer = banInfo.Users.FirstOrNull()?.UserId;
if (firstPlayer == null)
return TimeSpan.Zero;
return (await _db.GetPlayTimes(firstPlayer.Value))
.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)
?.TimeSpent ?? TimeSpan.Zero;
}
private IEnumerable<BanRoleDef> ToBanRoleDef<T>(IEnumerable<ProtoId<T>> protoIds) where T : class, IPrototype
{
return protoIds.Select(protoId =>
{
// TODO: I have no idea if this check is necessary. The previous code was a complete mess,
// so out of safety I'm leaving this in.
if (_prototypeManager.HasIndex<JobPrototype>(protoId) && _prototypeManager.HasIndex<AntagPrototype>(protoId))
{
throw new InvalidOperationException(
$"Creating role ban for {protoId}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
}
// 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<T>(), protoId);
});
}
private static string PrototypeKindToDbType<T>() where T : class, IPrototype
{
if (typeof(T) == typeof(JobPrototype))
return DbTypeJob;
if (typeof(T) == typeof(AntagPrototype))
return DbTypeAntag;
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)
{
if (_playerManager.TryGetSessionById(user, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
cachedBans.Add(banDef);
}
}
} }
public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
{ {
var ban = await _db.GetServerRoleBanAsync(banId); var ban = await _db.GetBanAsync(banId);
if (ban == null) if (ban == null)
{ {
return $"No ban found with id {banId}"; 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) if (ban.Unban != null)
{ {
var response = new StringBuilder("This ban has already been pardoned"); var response = new StringBuilder("This ban has already been pardoned");
@@ -326,14 +384,17 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
return response.ToString(); 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 foreach (var user in ban.UserIds)
&& _playerManager.TryGetSessionById(player, out var session)
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
{ {
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); if (_playerManager.TryGetSessionById(user, out var session)
SendRoleBans(session); && _cachedRoleBans.TryGetValue(session, out var roleBans))
{
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
SendRoleBans(session);
}
} }
return $"Pardoned ban with id {banId}"; return $"Pardoned ban with id {banId}";
@@ -341,64 +402,69 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId) public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{ {
return GetRoleBans<JobPrototype>(playerUserId, PrefixJob); return GetRoleBans<JobPrototype>(playerUserId);
} }
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId) public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
{ {
return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag); return GetRoleBans<AntagPrototype>(playerUserId);
} }
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId) where T : class, IPrototype
{ {
if (!_playerManager.TryGetSessionById(playerUserId, out var session)) if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null; return null;
return GetRoleBans<T>(session, prefix); return GetRoleBans<T>(session);
} }
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession) where T : class, IPrototype
{ {
if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans)) if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
return null; return null;
var dbType = PrototypeKindToDbType<T>();
return roleBans return roleBans
.Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal)) .SelectMany(ban => ban.Roles!.Value)
.Select(ban => new ProtoId<T>(ban.Role[prefix.Length..])) .Where(role => role.RoleType == dbType)
.Select(role => new ProtoId<T>(role.RoleId))
.ToHashSet(); .ToHashSet();
} }
public HashSet<string>? GetRoleBans(NetUserId playerUserId) public HashSet<BanRoleDef>? GetRoleBans(NetUserId playerUserId)
{ {
if (!_playerManager.TryGetSessionById(playerUserId, out var session)) if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null; return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans) return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet() ? roleBans.SelectMany(banDef => banDef.Roles ?? []).ToHashSet()
: null; : null;
} }
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs) public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
{ {
return IsRoleBanned(player, jobs, PrefixJob); return IsRoleBanned<JobPrototype>(player, jobs);
} }
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags) public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
{ {
return IsRoleBanned(player, antags, PrefixAntag); return IsRoleBanned<AntagPrototype>(player, antags);
} }
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles) where T : class, IPrototype
{ {
var bans = GetRoleBans(player.UserId); var bans = GetRoleBans(player.UserId);
if (bans is null || bans.Count == 0) if (bans is null || bans.Count == 0)
return false; return false;
var dbType = PrototypeKindToDbType<T>();
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var role in roles) foreach (var role in roles)
{ {
if (bans.Contains(prefix + role)) if (bans.Contains(new BanRoleDef(dbType, role)))
return true; return true;
} }
@@ -407,34 +473,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public void SendRoleBans(ICommonSession pSession) public void SendRoleBans(ICommonSession pSession)
{ {
var jobBans = GetRoleBans<JobPrototype>(pSession, PrefixJob);
var jobBansList = new List<string>(jobBans?.Count ?? 0);
if (jobBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in jobBans)
{
jobBansList.Add(encodedId.ToString().Replace(PrefixJob, ""));
}
}
var antagBans = GetRoleBans<AntagPrototype>(pSession, PrefixAntag);
var antagBansList = new List<string>(antagBans?.Count ?? 0);
if (antagBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in antagBans)
{
antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, ""));
}
}
var bans = new MsgRoleBans() var bans = new MsgRoleBans()
{ {
JobBans = jobBansList, JobBans = (GetRoleBans<JobPrototype>(pSession) ?? []).ToList(),
AntagBans = antagBansList, AntagBans = (GetRoleBans<AntagPrototype>(pSession) ?? []).ToList(),
}; };
_sawmill.Debug($"Sent role bans to {pSession.Name}"); _sawmill.Debug($"Sent role bans to {pSession.Name}");

View File

@@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -13,6 +14,11 @@ public interface IBanManager
public void Initialize(); public void Initialize();
public void Restart(); public void Restart();
/// <summary>
/// Create a server ban in the database, blocking connection for matching players.
/// </summary>
void CreateServerBan(CreateServerBanInfo banInfo);
/// <summary> /// <summary>
/// Bans the specified target, address range and / or HWID. One of them must be non-null /// Bans the specified target, address range and / or HWID. One of them must be non-null
/// </summary> /// </summary>
@@ -23,12 +29,44 @@ public interface IBanManager
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param> /// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param> /// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param> /// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); [Obsolete("Use CreateServerBan(CreateBanInfo) instead")]
public void CreateServerBan(NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
uint? minutes,
NoteSeverity severity,
string reason)
{
var info = new CreateServerBanInfo(reason);
if (target != null)
{
ArgumentNullException.ThrowIfNull(targetUsername);
info.AddUser(target.Value, targetUsername);
}
if (addressRange != null)
info.AddAddressRange(addressRange.Value);
if (hwid != null)
info.AddHWId(hwid);
if (minutes > 0)
info.WithMinutes(minutes.Value);
if (banningAdmin != null)
info.WithBanningAdmin(banningAdmin.Value);
info.WithSeverity(severity);
CreateServerBan(info);
}
/// <summary> /// <summary>
/// Gets a list of prefixed prototype IDs with the player's role bans. /// Gets a list of prefixed prototype IDs with the player's role bans.
/// </summary> /// </summary>
public HashSet<string>? GetRoleBans(NetUserId playerUserId); public HashSet<BanRoleDef>? GetRoleBans(NetUserId playerUserId);
/// <summary> /// <summary>
/// Checks if the player is currently banned from any of the listed roles. /// Checks if the player is currently banned from any of the listed roles.
@@ -57,33 +95,12 @@ public interface IBanManager
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId); public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
/// <summary> /// <summary>
/// Creates a job ban for the specified target, username or GUID /// Creates a role ban, preventing matching players from playing said roles.
/// </summary> /// </summary>
/// <param name="target">Target user, username or GUID, null for none</param> public void CreateRoleBan(CreateRoleBanInfo banInfo);
/// <param name="targetUsername">The username of the target, if known</param>
/// <param name="banningAdmin">The responsible admin for the ban</param>
/// <param name="addressRange">The range of IPs that are to be banned, if known</param>
/// <param name="hwid">The HWID to be banned, if known</param>
/// <param name="role">The role ID to be banned from. Either an AntagPrototype or a JobPrototype</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan<T>(
NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
ProtoId<T> role,
uint? minutes,
NoteSeverity severity,
string reason,
DateTimeOffset timeOfBan
) where T : class, IPrototype;
/// <summary> /// <summary>
/// Pardons a role ban for the specified target, username or GUID /// Pardons a role ban by its ID.
/// </summary> /// </summary>
/// <param name="banId">The id of the role ban to pardon.</param> /// <param name="banId">The id of the role ban to pardon.</param>
/// <param name="unbanningAdmin">The admin, if any, that pardoned the role ban.</param> /// <param name="unbanningAdmin">The admin, if any, that pardoned the role ban.</param>
@@ -96,3 +113,287 @@ public interface IBanManager
/// <param name="pSession">Player's session</param> /// <param name="pSession">Player's session</param>
public void SendRoleBans(ICommonSession pSession); public void SendRoleBans(ICommonSession pSession);
} }
/// <summary>
/// Base info to fill out in created ban records.
/// </summary>
/// <seealso cref="CreateServerBanInfo"/>
/// <seealso cref="CreateRoleBanInfo"/>
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
public abstract class CreateBanInfo
{
[Access(Other = AccessPermissions.Read)]
public const int DefaultMaskIpv4 = 32;
[Access(Other = AccessPermissions.Read)]
public const int DefaultMaskIpv6 = 64;
internal readonly HashSet<(NetUserId UserId, string UserName)> Users = [];
internal readonly HashSet<(IPAddress Address, int Mask)> AddressRanges = [];
internal readonly HashSet<ImmutableTypedHwid> HWIds = [];
internal readonly HashSet<int> RoundIds = [];
internal TimeSpan? Duration;
internal NoteSeverity? Severity;
internal string Reason;
internal NetUserId? BanningAdmin;
protected CreateBanInfo(string reason)
{
Reason = reason;
}
/// <summary>
/// Add a user to be matched by the ban.
/// </summary>
/// <remarks>
/// Bans can target multiple users at once.
/// </remarks>
/// <param name="userId">The ID of the user.</param>
/// <param name="username">The name of the user (used for logging purposes).</param>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddUser(NetUserId userId, string username)
{
Users.Add((userId, username));
return this;
}
/// <summary>
/// Add an IP address to be matched by the ban.
/// </summary>
/// <remarks>
/// Bans can target multiple addresses at once.
/// </remarks>
/// <param name="address">
/// The IP address to add. If null, nothing is done.
/// </param>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddAddress(IPAddress? address)
{
if (address == null)
return this;
return AddAddressRange(
address,
address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6);
}
/// <summary>
/// Add an IP address range to be matched by the ban.
/// </summary>
/// <remarks>
/// Bans can target multiple address ranges at once.
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange)
{
return AddAddressRange(addressRange.Address, addressRange.Mask);
}
/// <summary>
/// Add an IP address range to be matched by the ban.
/// </summary>
/// <remarks>
/// Bans can target multiple address ranges at once.
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddAddressRange(IPAddress address, int mask)
{
AddressRanges.Add((address, mask));
return this;
}
/// <summary>
/// Add a hardware IP (HWID) to be matched by the ban.
/// </summary>
/// <remarks>
/// Bans can target multiple HWIDs at once.
/// </remarks>
/// <param name="hwId">
/// The HWID to add. If null, nothing is done.
/// </param>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId)
{
if (hwId != null)
HWIds.Add(hwId);
return this;
}
/// <summary>
/// Add a relevant round ID to this ban.
/// </summary>
/// <remarks>
/// <para>
/// If not specified, the current round ID is used for the ban.
/// Therefore, the first call to this function will <i>replace</i> the round ID,
/// and further calls will add additional round IDs.
/// </para>
/// <para>
/// Bans can target multiple round IDs at once.
/// </para>
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo AddRoundId(int roundId)
{
RoundIds.Add(roundId);
return this;
}
/// <summary>
/// Set how long the ban will last, in minutes.
/// </summary>
/// <remarks>
/// If no duration is specified, the ban is permanent.
/// </remarks>
/// <param name="minutes">The duration of the ban, in minutes.</param>
/// <returns>The current object, for easy chaining.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <see cref="minutes"/> is not a positive number.
/// </exception>
public CreateBanInfo WithMinutes(int minutes)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
return WithMinutes((uint)minutes);
}
/// <summary>
/// Set how long the ban will last, in minutes.
/// </summary>
/// <remarks>
/// If no duration is specified, the ban is permanent.
/// </remarks>
/// <param name="minutes">The duration of the ban, in minutes.</param>
/// <returns>The current object, for easy chaining.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <see cref="minutes"/> is not a positive number.
/// </exception>
public CreateBanInfo WithMinutes(uint minutes)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
return WithDuration(TimeSpan.FromMinutes(minutes));
}
/// <summary>
/// Set how long the ban will last.
/// </summary>
/// <remarks>
/// If no duration is specified, the ban is permanent.
/// </remarks>
/// <param name="duration">The duration of the ban.</param>
/// <returns>The current object, for easy chaining.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <see cref="duration"/> is not a positive amount of time.
/// </exception>
public CreateBanInfo WithDuration(TimeSpan duration)
{
if (duration <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero.");
Duration = duration;
return this;
}
/// <summary>
/// Set the severity of the ban.
/// </summary>
/// <remarks>
/// If no severity is specified, the default is specified through server configuration.
/// </remarks>
/// <param name="severity"></param>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo WithSeverity(NoteSeverity severity)
{
Severity = severity;
return this;
}
/// <summary>
/// Set the reason for the ban.
/// </summary>
/// <remarks>
/// This replaces the value given via the object constructor.
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo WithReason(string reason)
{
Reason = reason;
return this;
}
/// <summary>
/// Specify the admin responsible for placing the ban.
/// </summary>
/// <returns>The current object, for easy chaining.</returns>
public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin)
{
BanningAdmin = banningAdmin;
return this;
}
}
/// <summary>
/// Stores info to create server ban records.
/// </summary>
/// <seealso cref="IBanManager.CreateServerBan(CreateServerBanInfo)"/>
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
public sealed class CreateServerBanInfo : CreateBanInfo
{
/// <param name="reason">The reason for the server ban.</param>
public CreateServerBanInfo(string reason) : base(reason)
{
}
}
/// <summary>
/// Stores info to create role ban records.
/// </summary>
/// <seealso cref="IBanManager.CreateRoleBan(CreateRoleBanInfo)"/>
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
public sealed class CreateRoleBanInfo : CreateBanInfo
{
internal readonly HashSet<ProtoId<AntagPrototype>> AntagPrototypes = [];
internal readonly HashSet<ProtoId<JobPrototype>> JobPrototypes = [];
/// <param name="reason">The reason for the role ban.</param>
public CreateRoleBanInfo(string reason) : base(reason)
{
}
/// <summary>
/// Add an antag role that will be unavailable for banned players.
/// </summary>
/// <remarks>
/// <para>
/// Bans can have multiple roles at once.
/// </para>
/// <para>
/// While not checked in this function, adding a ban with invalid role IDs will cause a
/// <see cref="UnknownPrototypeException"/> when actually creating the ban.
/// </para>
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateRoleBanInfo AddAntag(ProtoId<AntagPrototype> protoId)
{
AntagPrototypes.Add(protoId);
return this;
}
/// <summary>
/// Add a job role that will be unavailable for banned players.
/// </summary>
/// <remarks>
/// <para>
/// Bans can have multiple roles at once.
/// </para>
/// <para>
/// While not checked in this function, adding a ban with invalid role IDs will cause a
/// <see cref="UnknownPrototypeException"/> when actually creating the ban.
/// </para>
/// </remarks>
/// <returns>The current object, for easy chaining.</returns>
public CreateRoleBanInfo AddJob(ProtoId<JobPrototype> protoId)
{
JobPrototypes.Add(protoId);
return this;
}
}

View File

@@ -22,7 +22,7 @@ public sealed class AdminNotesEui : BaseEui
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
} }
private Guid NotedPlayer { get; set; } private NetUserId NotedPlayer { get; set; }
private string NotedPlayerName { get; set; } = string.Empty; private string NotedPlayerName { get; set; } = string.Empty;
private bool HasConnectedBefore { get; set; } private bool HasConnectedBefore { get; set; }
private Dictionary<(int, NoteType), SharedAdminNote> Notes { get; set; } = new(); private Dictionary<(int, NoteType), SharedAdminNote> Notes { get; set; } = new();
@@ -112,7 +112,7 @@ public sealed class AdminNotesEui : BaseEui
} }
} }
public async Task ChangeNotedPlayer(Guid notedPlayer) public async Task ChangeNotedPlayer(NetUserId notedPlayer)
{ {
NotedPlayer = notedPlayer; NotedPlayer = notedPlayer;
await LoadFromDb(); await LoadFromDb();
@@ -120,7 +120,7 @@ public sealed class AdminNotesEui : BaseEui
private void NoteModified(SharedAdminNote note) private void NoteModified(SharedAdminNote note)
{ {
if (note.Player != NotedPlayer) if (!note.Players.Contains(NotedPlayer))
return; return;
Notes[(note.Id, note.NoteType)] = note; Notes[(note.Id, note.NoteType)] = note;
@@ -129,7 +129,7 @@ public sealed class AdminNotesEui : BaseEui
private void NoteDeleted(SharedAdminNote note) private void NoteDeleted(SharedAdminNote note)
{ {
if (note.Player != NotedPlayer) if (!note.Players.Contains(NotedPlayer))
return; return;
Notes.Remove((note.Id, note.NoteType)); Notes.Remove((note.Id, note.NoteType));

View File

@@ -1,3 +1,5 @@
using System.Collections.Immutable;
using System.Linq;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.Administration.Notes; using Content.Shared.Administration.Notes;
using Content.Shared.Database; using Content.Shared.Database;
@@ -11,7 +13,7 @@ public static class AdminNotesExtensions
NoteSeverity? severity = null; NoteSeverity? severity = null;
var secret = false; var secret = false;
NoteType type; NoteType type;
string[]? bannedRoles = null; ImmutableArray<BanRoleDef>? bannedRoles = null;
string? unbannedByName = null; string? unbannedByName = null;
DateTime? unbannedTime = null; DateTime? unbannedTime = null;
bool? seen = null; bool? seen = null;
@@ -30,13 +32,13 @@ public static class AdminNotesExtensions
type = NoteType.Message; type = NoteType.Message;
seen = adminMessage.Seen; seen = adminMessage.Seen;
break; break;
case ServerBanNoteRecord ban: case BanNoteRecord { Type: BanType.Server } ban:
type = NoteType.ServerBan; type = NoteType.ServerBan;
severity = ban.Severity; severity = ban.Severity;
unbannedTime = ban.UnbanTime; unbannedTime = ban.UnbanTime;
unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user"); unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user");
break; break;
case ServerRoleBanNoteRecord roleBan: case BanNoteRecord { Type: BanType.Role } roleBan:
type = NoteType.RoleBan; type = NoteType.RoleBan;
severity = roleBan.Severity; severity = roleBan.Severity;
bannedRoles = roleBan.Roles; bannedRoles = roleBan.Roles;
@@ -48,14 +50,14 @@ public static class AdminNotesExtensions
} }
// There may be bans without a user, but why would we ever be converting them to shared notes? // There may be bans without a user, but why would we ever be converting them to shared notes?
if (note.Player is null) if (note.Players.Length == 0)
throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); throw new ArgumentNullException(nameof(note), "Player user ID cannot be empty for a note");
return new SharedAdminNote( return new SharedAdminNote(
note.Id, note.Id,
note.Player!.UserId, [..note.Players.Select(p => p.UserId)],
note.Round?.Id, [..note.Rounds.Select(r => r.Id)],
note.Round?.Server.Name, note.Rounds.SingleOrDefault()?.Server.Name, // TODO: Show all server names?
note.PlaytimeAtNote, note.PlaytimeAtNote,
type, type,
note.Message, note.Message,

View File

@@ -52,7 +52,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
return _admins.HasAdminFlag(admin, AdminFlags.ViewNotes); 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(); var ui = new AdminNotesEui();
_euis.OpenEui(ui, admin); _euis.OpenEui(ui, admin);
@@ -144,8 +144,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
var note = new SharedAdminNote( var note = new SharedAdminNote(
noteId, noteId,
(NetUserId) player, [(NetUserId) player],
roundId, roundId.HasValue ? [roundId.Value] : [],
serverName, serverName,
playtime, playtime,
type, type,
@@ -172,8 +172,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
NoteType.Note => (await _db.GetAdminNote(id))?.ToShared(), NoteType.Note => (await _db.GetAdminNote(id))?.ToShared(),
NoteType.Watchlist => (await _db.GetAdminWatchlist(id))?.ToShared(), NoteType.Watchlist => (await _db.GetAdminWatchlist(id))?.ToShared(),
NoteType.Message => (await _db.GetAdminMessage(id))?.ToShared(), NoteType.Message => (await _db.GetAdminMessage(id))?.ToShared(),
NoteType.ServerBan => (await _db.GetServerBanAsNoteAsync(id))?.ToShared(), NoteType.ServerBan or NoteType.RoleBan => (await _db.GetBanAsNoteAsync(id))?.ToShared(),
NoteType.RoleBan => (await _db.GetServerRoleBanAsNoteAsync(id))?.ToShared(),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type") _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type")
}; };
} }
@@ -200,11 +199,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
case NoteType.Message: case NoteType.Message:
await _db.DeleteAdminMessage(noteId, deletedBy.UserId, deletedAt); await _db.DeleteAdminMessage(noteId, deletedBy.UserId, deletedAt);
break; break;
case NoteType.ServerBan: case NoteType.ServerBan or NoteType.RoleBan:
await _db.HideServerBanFromNotes(noteId, deletedBy.UserId, deletedAt); await _db.HideBanFromNotes(noteId, deletedBy.UserId, deletedAt);
break;
case NoteType.RoleBan:
await _db.HideServerRoleBanFromNotes(noteId, deletedBy.UserId, deletedAt);
break; break;
default: default:
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type");
@@ -280,15 +276,10 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
case NoteType.Message: case NoteType.Message:
await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime); await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime);
break; break;
case NoteType.ServerBan: case NoteType.ServerBan or NoteType.RoleBan:
if (severity is null) if (severity is null)
throw new ArgumentException("Severity cannot be null for a ban", nameof(severity)); throw new ArgumentException("Severity cannot be null for a ban", nameof(severity));
await _db.EditServerBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); await _db.EditBan(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);
break; break;
default: default:
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type");

View File

@@ -2,6 +2,7 @@ using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.Administration.Notes; using Content.Shared.Administration.Notes;
using Content.Shared.Database; using Content.Shared.Database;
using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
namespace Content.Server.Administration.Notes; namespace Content.Server.Administration.Notes;
@@ -16,7 +17,7 @@ public interface IAdminNotesManager
bool CanDelete(ICommonSession admin); bool CanDelete(ICommonSession admin);
bool CanEdit(ICommonSession admin); bool CanEdit(ICommonSession admin);
bool CanView(ICommonSession admin); bool CanView(ICommonSession admin);
Task OpenEui(ICommonSession admin, Guid notedPlayer); Task OpenEui(ICommonSession admin, NetUserId notedPlayer);
Task OpenUserNotesEui(ICommonSession player); Task OpenUserNotesEui(ICommonSession player);
Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime); Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime);
Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy); Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy);

View File

@@ -186,11 +186,8 @@ public sealed class PlayerPanelEui : BaseEui
{ {
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId); _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways // 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; _bans = (await _db.GetBansAsync(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 _roleBans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null, type: BanType.Role)).Count();
// 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();
} }
else else
{ {

View File

@@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems
} }
// Check if the user has been banned // 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) if (ban != null)
{ {
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason)); var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));

View File

@@ -207,7 +207,7 @@ namespace Content.Server.Connection
* TODO: Jesus H Christ what is this utter mess of a function * TODO: Jesus H Christ what is this utter mess of a function
* TODO: Break this apart into is constituent steps. * TODO: Break this apart into is constituent steps.
*/ */
private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny( private async Task<(ConnectionDenyReason, string, List<BanDef>? bansHit)?> ShouldDeny(
NetConnectingArgs e) NetConnectingArgs e)
{ {
// Check if banned. // Check if banned.
@@ -228,7 +228,7 @@ namespace Content.Server.Connection
return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null); 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) if (bans.Count > 0)
{ {
var firstBan = bans[0]; var firstBan = bans[0];

View File

@@ -0,0 +1,128 @@
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
namespace Content.Server.Database
{
public sealed class BanDef
{
public int? Id { get; }
public BanType Type { get; }
public ImmutableArray<NetUserId> UserIds { get; }
public ImmutableArray<(IPAddress address, int cidrMask)> Addresses { get; }
public ImmutableArray<ImmutableTypedHwid> HWIds { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
public ImmutableArray<int> RoundIds { get; }
public TimeSpan PlaytimeAtNote { get; }
public string Reason { get; }
public NoteSeverity Severity { get; set; }
public NetUserId? BanningAdmin { get; }
public UnbanDef? Unban { get; }
public ServerBanExemptFlags ExemptFlags { get; }
public ImmutableArray<BanRoleDef>? Roles { get; }
public BanDef(
int? id,
BanType type,
ImmutableArray<NetUserId> userIds,
ImmutableArray<(IPAddress address, int cidrMask)> addresses,
ImmutableArray<ImmutableTypedHwid> hwIds,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
ImmutableArray<int> roundIds,
TimeSpan playtimeAtNote,
string reason,
NoteSeverity severity,
NetUserId? banningAdmin,
UnbanDef? unban,
ServerBanExemptFlags exemptFlags = default,
ImmutableArray<BanRoleDef>? roles = null)
{
if (userIds.Length == 0 && addresses.Length == 0 && hwIds.Length == 0)
{
throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID");
}
addresses = addresses.Select(address =>
{
if (address is { address.IsIPv4MappedToIPv6: true } addr)
{
// Fix IPv6-mapped IPv4 addresses
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
address = (addr.address.MapToIPv4(), addr.cidrMask - 96);
}
return address;
})
.ToImmutableArray();
Id = id;
Type = type;
UserIds = userIds;
Addresses = addresses;
HWIds = hwIds;
BanTime = banTime;
ExpirationTime = expirationTime;
RoundIds = roundIds;
PlaytimeAtNote = playtimeAtNote;
Reason = reason;
Severity = severity;
BanningAdmin = banningAdmin;
Unban = unban;
ExemptFlags = exemptFlags;
switch (Type)
{
case BanType.Server:
if (roles != null)
throw new ArgumentException("Cannot specify roles for server ban types", nameof(roles));
break;
case BanType.Role:
if (roles is not { Length: > 0 })
throw new ArgumentException("Must specify roles for server ban types", nameof(roles));
if (exemptFlags != 0)
throw new ArgumentException("Role bans cannot have exempt flags", nameof(exemptFlags));
break;
default:
throw new ArgumentOutOfRangeException(nameof(type));
}
Roles = roles;
}
public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc)
{
string expires;
if (ExpirationTime is { } expireTime)
{
var duration = expireTime - BanTime;
var utc = expireTime.ToUniversalTime();
expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f")));
}
else
{
var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal);
expires = !string.IsNullOrWhiteSpace(appeal)
? loc.GetString("ban-banned-permanent-appeal", ("link", appeal))
: loc.GetString("ban-banned-permanent");
}
return $"""
{loc.GetString("ban-banned-1")}
{loc.GetString("ban-banned-2", ("reason", Reason))}
{expires}
{loc.GetString("ban-banned-3")}
""";
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;
using System.Net; using System.Net;
using Content.Server.IP; using Content.Server.IP;
using Content.Shared.Database; using Content.Shared.Database;
@@ -7,7 +8,7 @@ using Robust.Shared.Network;
namespace Content.Server.Database; namespace Content.Server.Database;
/// <summary> /// <summary>
/// Implements logic to match a <see cref="ServerBanDef"/> against a player query. /// Implements logic to match a <see cref="BanDef"/> against a player query.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
@@ -29,7 +30,7 @@ public static class BanMatcher
/// <param name="ban">The ban information.</param> /// <param name="ban">The ban information.</param>
/// <param name="player">Information about the player to match against.</param> /// <param name="player">Information about the player to match against.</param>
/// <returns>True if the ban matches the provided player info.</returns> /// <returns>True if the ban matches the provided player info.</returns>
public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) public static bool BanMatches(BanDef ban, in PlayerInfo player)
{ {
var exemptFlags = player.ExemptFlags; var exemptFlags = player.ExemptFlags;
// Any flag to bypass BlacklistedRange bans. // Any flag to bypass BlacklistedRange bans.
@@ -39,39 +40,44 @@ public static class BanMatcher
if ((ban.ExemptFlags & exemptFlags) != 0) if ((ban.ExemptFlags & exemptFlags) != 0)
return false; return false;
var playerAddr = player.Address;
if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP) if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP)
&& player.Address != null && playerAddr != null
&& ban.Address is not null && ban.Addresses.Any(addr => playerAddr.IsInSubnet(addr))
&& player.Address.IsInSubnet(ban.Address.Value)
&& (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer)) && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer))
{ {
return true; return true;
} }
if (player.UserId is { } id && ban.UserId == id.UserId) if (player.UserId is { } id && ban.UserIds.Contains(id))
{ {
return true; return true;
} }
switch (ban.HWId?.Type) foreach (var banHwid in ban.HWIds)
{ {
case HwidType.Legacy: switch (banHwid.Type)
if (player.HWId is { Length: > 0 } hwIdVar {
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) case HwidType.Legacy:
{ if (player.HWId is { Length: > 0 } hwIdVar
return true; && hwIdVar.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan()))
}
break;
case HwidType.Modern:
if (player.ModernHWIds is { Length: > 0 } modernHwIdVar)
{
foreach (var hwid in modernHwIdVar)
{ {
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; return false;

View File

@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Net; using System.Net;
using Content.Shared.Database; using Content.Shared.Database;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -12,9 +13,9 @@ public interface IAdminRemarksRecord
{ {
public int Id { get; } public int Id { get; }
public RoundRecord? Round { get; } public ImmutableArray<RoundRecord> Rounds { get; }
public PlayerRecord? Player { get; } public ImmutableArray<PlayerRecord> Players { get; }
public TimeSpan PlaytimeAtNote { get; } public TimeSpan PlaytimeAtNote { get; }
public string Message { get; } public string Message { get; }
@@ -31,27 +32,11 @@ public interface IAdminRemarksRecord
public bool Deleted { get; } public bool Deleted { get; }
} }
public sealed record ServerRoleBanNoteRecord( public sealed record BanNoteRecord(
int Id, int Id,
RoundRecord? Round, BanType Type,
PlayerRecord? Player, ImmutableArray<RoundRecord> Rounds,
TimeSpan PlaytimeAtNote, ImmutableArray<PlayerRecord> Players,
string Message,
NoteSeverity Severity,
PlayerRecord? CreatedBy,
DateTimeOffset CreatedAt,
PlayerRecord? LastEditedBy,
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, TimeSpan PlaytimeAtNote,
string Message, string Message,
NoteSeverity Severity, NoteSeverity Severity,
@@ -62,7 +47,8 @@ public sealed record ServerBanNoteRecord(
DateTimeOffset? ExpirationTime, DateTimeOffset? ExpirationTime,
bool Deleted, bool Deleted,
PlayerRecord? UnbanningAdmin, PlayerRecord? UnbanningAdmin,
DateTime? UnbanTime) : IAdminRemarksRecord; DateTime? UnbanTime,
ImmutableArray<BanRoleDef> Roles) : IAdminRemarksRecord;
public sealed record AdminNoteRecord( public sealed record AdminNoteRecord(
int Id, int Id,
@@ -79,7 +65,11 @@ public sealed record AdminNoteRecord(
bool Deleted, bool Deleted,
PlayerRecord? DeletedBy, PlayerRecord? DeletedBy,
DateTimeOffset? DeletedAt, DateTimeOffset? DeletedAt,
bool Secret) : IAdminRemarksRecord; bool Secret) : IAdminRemarksRecord
{
ImmutableArray<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
ImmutableArray<PlayerRecord> IAdminRemarksRecord.Players => Player != null ? [Player] : [];
}
public sealed record AdminWatchlistRecord( public sealed record AdminWatchlistRecord(
int Id, int Id,
@@ -94,7 +84,11 @@ public sealed record AdminWatchlistRecord(
DateTimeOffset? ExpirationTime, DateTimeOffset? ExpirationTime,
bool Deleted, bool Deleted,
PlayerRecord? DeletedBy, PlayerRecord? DeletedBy,
DateTimeOffset? DeletedAt) : IAdminRemarksRecord; DateTimeOffset? DeletedAt) : IAdminRemarksRecord
{
ImmutableArray<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
ImmutableArray<PlayerRecord> IAdminRemarksRecord.Players => Player != null ? [Player] : [];
}
public sealed record AdminMessageRecord( public sealed record AdminMessageRecord(
int Id, int Id,
@@ -111,15 +105,18 @@ public sealed record AdminMessageRecord(
PlayerRecord? DeletedBy, PlayerRecord? DeletedBy,
DateTimeOffset? DeletedAt, DateTimeOffset? DeletedAt,
bool Seen, bool Seen,
bool Dismissed) : IAdminRemarksRecord; bool Dismissed) : IAdminRemarksRecord
{
ImmutableArray<RoundRecord> IAdminRemarksRecord.Rounds => Round != null ? [Round] : [];
ImmutableArray<PlayerRecord> IAdminRemarksRecord.Players => Player != null ? [Player] : [];
}
public sealed record PlayerRecord( public sealed record PlayerRecord(
NetUserId UserId, NetUserId UserId,
DateTimeOffset FirstSeenTime, DateTimeOffset FirstSeenTime,
string LastSeenUserName, string LastSeenUserName,
DateTimeOffset LastSeenTime, DateTimeOffset LastSeenTime,
IPAddress LastSeenAddress, IPAddress? LastSeenAddress,
ImmutableTypedHwid? HWId); ImmutableTypedHwid? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server); public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);

View File

@@ -0,0 +1,37 @@
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database;
internal static class EFCoreExtensions
{
extension<TEntity>(IQueryable<TEntity> query) where TEntity : class
{
public IQueryable<TEntity> ApplyIncludes(
IEnumerable<Expression<Func<TEntity, object>>> properties)
{
var q = query;
foreach (var property in properties)
{
q = q.Include(property);
}
return q;
}
public IQueryable<TEntity> ApplyIncludes<TDerived>(
IEnumerable<Expression<Func<TDerived, object>>> properties,
Expression<Func<TEntity, TDerived>> getDerived)
where TDerived : class
{
var q = query;
foreach (var property in properties)
{
q = q.Include(getDerived).ThenInclude(property);
}
return q;
}
}
}

View File

@@ -1,93 +0,0 @@
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
namespace Content.Server.Database
{
public sealed class ServerBanDef
{
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
public int? RoundId { get; }
public TimeSpan PlaytimeAtNote { get; }
public string Reason { get; }
public NoteSeverity Severity { get; set; }
public NetUserId? BanningAdmin { get; }
public ServerUnbanDef? Unban { get; }
public ServerBanExemptFlags ExemptFlags { get; }
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
TypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,
TimeSpan playtimeAtNote,
string reason,
NoteSeverity severity,
NetUserId? banningAdmin,
ServerUnbanDef? unban,
ServerBanExemptFlags exemptFlags = default)
{
if (userId == null && address == null && hwId == null)
{
throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID");
}
if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6)
{
// Fix IPv6-mapped IPv4 addresses
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
address = (addr.Item1.MapToIPv4(), addr.Item2 - 96);
}
Id = id;
UserId = userId;
Address = address;
HWId = hwId;
BanTime = banTime;
ExpirationTime = expirationTime;
RoundId = roundId;
PlaytimeAtNote = playtimeAtNote;
Reason = reason;
Severity = severity;
BanningAdmin = banningAdmin;
Unban = unban;
ExemptFlags = exemptFlags;
}
public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc)
{
string expires;
if (ExpirationTime is { } expireTime)
{
var duration = expireTime - BanTime;
var utc = expireTime.ToUniversalTime();
expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f")));
}
else
{
var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal);
expires = !string.IsNullOrWhiteSpace(appeal)
? loc.GetString("ban-banned-permanent-appeal", ("link", appeal))
: loc.GetString("ban-banned-permanent");
}
return $"""
{loc.GetString("ban-banned-1")}
{loc.GetString("ban-banned-2", ("reason", Reason))}
{expires}
{loc.GetString("ban-banned-3")}
""";
}
}
}

View File

@@ -1,13 +1,13 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Net; using System.Net;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Body; using Content.Shared.Body;
using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Prototypes;
@@ -457,7 +457,7 @@ namespace Content.Server.Database
/// </summary> /// </summary>
/// <param name="id">The ban id to look for.</param> /// <param name="id">The ban id to look for.</param>
/// <returns>The ban with the given id or null if none exist.</returns> /// <returns>The ban with the given id or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(int id); public abstract Task<BanDef?> GetBanAsync(int id);
/// <summary> /// <summary>
/// Looks up an user's most recent received un-pardoned ban. /// Looks up an user's most recent received un-pardoned ban.
@@ -469,11 +469,12 @@ namespace Content.Server.Database
/// <param name="hwId">The legacy HWId of the user.</param> /// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns> /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync( public abstract Task<BanDef?> GetBanAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds); ImmutableArray<ImmutableArray<byte>>? modernHWIds,
BanType type);
/// <summary> /// <summary>
/// Looks up an user's ban history. /// Looks up an user's ban history.
@@ -486,17 +487,18 @@ namespace Content.Server.Database
/// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param> /// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns> /// <returns>The user's ban history.</returns>
public abstract Task<List<ServerBanDef>> GetServerBansAsync( public abstract Task<List<BanDef>> GetBansAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned); bool includeUnbanned,
BanType type);
public abstract Task AddServerBanAsync(ServerBanDef serverBan); public abstract Task<BanDef> AddBanAsync(BanDef ban);
public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); 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(); await using var db = await GetDb();
@@ -559,61 +561,23 @@ namespace Content.Server.Database
return flags ?? ServerBanExemptFlags.None; return flags ?? ServerBanExemptFlags.None;
} }
#endregion protected static List<Expression<Func<Ban, object>>> GetBanDefIncludes(BanType? type = null)
#region Role Bans
/*
* ROLE BANS
*/
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
/// </summary>
/// <param name="id">The role ban id to look for.</param>
/// <returns>The role ban with the given id or null if none exist.</returns>
public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
/// <summary>
/// Looks up an user's role ban history.
/// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
/// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
/// </summary>
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{ {
await using var db = await GetDb(); List<Expression<Func<Ban, object>>> list =
var roleBanDetails = await db.DbContext.RoleBan [
.Where(b => b.Id == id) b => b.Players!,
.Select(b => new { b.BanTime, b.PlayerUserId }) b => b.Rounds!,
.SingleOrDefaultAsync(); b => b.Hwids!,
b => b.Unban!,
b => b.Addresses!,
];
if (roleBanDetails == default) if (type != BanType.Server)
return; list.Add(b => b.Roles!);
await db.DbContext.RoleBan return list;
.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)
);
} }
#endregion #endregion
#region Playtime #region Playtime
@@ -734,6 +698,19 @@ namespace Content.Server.Database
if (player == null) if (player == null)
return 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( return new PlayerRecord(
new NetUserId(player.UserId), new NetUserId(player.UserId),
new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)), new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)),
@@ -757,7 +734,7 @@ namespace Content.Server.Database
ConnectionDenyReason? denied, ConnectionDenyReason? denied,
int serverId); int serverId);
public async Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans) public async Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> bans)
{ {
await using var db = await GetDb(); await using var db = await GetDb();
@@ -1371,81 +1348,17 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
entity.Dismissed); entity.Dismissed);
} }
public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id) public async Task<BanNoteRecord?> GetBanAsNoteAsync(int id)
{ {
await using var db = await GetDb(); await using var db = await GetDb();
var ban = await db.DbContext.Ban var ban = await BanRecordQuery(db.DbContext)
.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); .SingleOrDefaultAsync(b => b.Id == id);
if (ban is null) if (ban is null)
return null; return null;
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); return await MakeBanNoteRecord(db.DbContext, ban);
return new ServerBanNoteRecord(
ban.Id,
MakeRoundRecord(ban.Round),
MakePlayerRecord(player),
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
MakePlayerRecord(ban.CreatedBy),
ban.BanTime,
MakePlayerRecord(ban.LastEditedBy),
ban.LastEditedAt,
ban.ExpirationTime,
ban.Hidden,
MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
? null
: await db.DbContext.Player.SingleOrDefaultAsync(p =>
p.UserId == ban.Unban.UnbanningAdmin.Value)),
ban.Unban?.UnbanTime);
}
public async Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
{
await using var db = await GetDb();
var ban = await db.DbContext.RoleBan
.Include(ban => ban.Unban)
.Include(ban => ban.Round)
.ThenInclude(r => r!.Server)
.Include(ban => ban.CreatedBy)
.Include(ban => ban.LastEditedBy)
.Include(ban => ban.Unban)
.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
return null;
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
var unbanningAdmin =
ban.Unban is null
? null
: await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin);
return new ServerRoleBanNoteRecord(
ban.Id,
MakeRoundRecord(ban.Round),
MakePlayerRecord(player),
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
MakePlayerRecord(ban.CreatedBy),
ban.BanTime,
MakePlayerRecord(ban.LastEditedBy),
ban.LastEditedAt,
ban.ExpirationTime,
ban.Hidden,
new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) },
MakePlayerRecord(unbanningAdmin),
ban.Unban?.UnbanTime);
} }
public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player) public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
@@ -1466,8 +1379,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.ToListAsync()).Select(MakeAdminNoteRecord)); .ToListAsync()).Select(MakeAdminNoteRecord));
notes.AddRange(await GetActiveWatchlistsImpl(db, player)); notes.AddRange(await GetActiveWatchlistsImpl(db, player));
notes.AddRange(await GetMessagesImpl(db, player)); notes.AddRange(await GetMessagesImpl(db, player));
notes.AddRange(await GetServerBansAsNotesForUser(db, player)); notes.AddRange(await GetBansAsNotesForUser(db, player));
notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
return notes; return notes;
} }
public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
@@ -1550,7 +1462,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync(); 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(); await using var db = await GetDb();
@@ -1563,19 +1475,6 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync(); await db.DbContext.SaveChangesAsync();
} }
public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync();
roleBan.Hidden = true;
roleBan.LastEditedById = deletedBy;
roleBan.LastEditedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player) public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player)
{ {
await using var db = await GetDb(); await using var db = await GetDb();
@@ -1593,8 +1492,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.Include(note => note.Player) .Include(note => note.Player)
.ToListAsync()).Select(MakeAdminNoteRecord)); .ToListAsync()).Select(MakeAdminNoteRecord));
notesCol.AddRange(await GetMessagesImpl(db, player)); notesCol.AddRange(await GetMessagesImpl(db, player));
notesCol.AddRange(await GetServerBansAsNotesForUser(db, player)); notesCol.AddRange(await GetBansAsNotesForUser(db, player));
notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
return notesCol; return notesCol;
} }
@@ -1657,43 +1555,65 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync(); await db.DbContext.SaveChangesAsync();
} }
private static IQueryable<Ban> BanRecordQuery(ServerDbContext dbContext)
{
return dbContext.Ban
.Include(ban => ban.Unban)
.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);
}
private async Task<BanNoteRecord> MakeBanNoteRecord(ServerDbContext dbContext, Ban ban)
{
var playerRecords = await AsyncSelect(ban.Players,
async bp => MakePlayerRecord(bp.UserId,
await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == bp.UserId)));
return 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))]);
}
// These two are here because they get converted into notes later // These two are here because they get converted into notes later
protected async Task<List<ServerBanNoteRecord>> GetServerBansAsNotesForUser(DbGuard db, Guid user) protected async Task<List<BanNoteRecord>> GetBansAsNotesForUser(DbGuard db, Guid user)
{ {
// You can't group queries, as player will not always exist. When it doesn't, the // You can't group queries, as player will not always exist. When it doesn't, the
// whole query returns nothing // whole query returns nothing
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); var bans = await BanRecordQuery(db.DbContext)
var bans = await db.DbContext.Ban .AsSplitQuery()
.Where(ban => ban.PlayerUserId == user && !ban.Hidden) .Where(ban => ban.Players!.Any(bp => bp.UserId == 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)
.ToArrayAsync(); .ToArrayAsync();
var banNotes = new List<ServerBanNoteRecord>(); var banNotes = new List<BanNoteRecord>();
foreach (var ban in bans) foreach (var ban in bans)
{ {
var banNote = new ServerBanNoteRecord( var banNote = await MakeBanNoteRecord(db.DbContext, ban);
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));
banNotes.Add(banNote); banNotes.Add(banNote);
} }
@@ -1701,56 +1621,6 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return banNotes; return banNotes;
} }
protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(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)
.ToArrayAsync();
// Client side query, as EF can't do groups yet
var bansEnumerable = bansQuery
.GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
.Select(banGroup => banGroup)
.ToArray();
List<ServerRoleBanNoteRecord> bans = new();
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
foreach (var banGroup in bansEnumerable)
{
var firstBan = banGroup.First();
Player? unbanningAdmin = null;
if (firstBan.Unban?.UnbanningAdmin is not null)
unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value);
bans.Add(new ServerRoleBanNoteRecord(
firstBan.Id,
MakeRoundRecord(firstBan.Round),
MakePlayerRecord(player),
firstBan.PlaytimeAtNote,
firstBan.Reason,
firstBan.Severity,
MakePlayerRecord(firstBan.CreatedBy),
NormalizeDatabaseTime(firstBan.BanTime),
MakePlayerRecord(firstBan.LastEditedBy),
NormalizeDatabaseTime(firstBan.LastEditedAt),
NormalizeDatabaseTime(firstBan.ExpirationTime),
firstBan.Hidden,
banGroup.Select(ban => ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null)).ToArray(),
MakePlayerRecord(unbanningAdmin),
NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
}
return bans;
}
#endregion #endregion
#region Job Whitelists #region Job Whitelists
@@ -1922,5 +1792,19 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
{ {
} }
private static async Task<IEnumerable<TResult>> AsyncSelect<T, TResult>(
IEnumerable<T>? enumerable,
Func<T, Task<TResult>> selector)
{
var results = new List<TResult>();
foreach (var item in enumerable ?? [])
{
results.Add(await selector(item));
}
return [..results];
}
} }
} }

View File

@@ -67,7 +67,7 @@ namespace Content.Server.Database
/// </summary> /// </summary>
/// <param name="id">The ban id to look for.</param> /// <param name="id">The ban id to look for.</param>
/// <returns>The ban with the given id or null if none exist.</returns> /// <returns>The ban with the given id or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync(int id); Task<BanDef?> GetBanAsync(int id);
/// <summary> /// <summary>
/// Looks up an user's most recent received un-pardoned ban. /// Looks up an user's most recent received un-pardoned ban.
@@ -79,11 +79,12 @@ namespace Content.Server.Database
/// <param name="hwId">The legacy HWID of the user.</param> /// <param name="hwId">The legacy HWID of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns> /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync( Task<BanDef?> GetBanAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds); ImmutableArray<ImmutableArray<byte>>? modernHWIds,
BanType type = BanType.Server);
/// <summary> /// <summary>
/// Looks up an user's ban history. /// Looks up an user's ban history.
@@ -95,17 +96,18 @@ namespace Content.Server.Database
/// <param name="modernHWIds">The modern HWIDs of the user.</param> /// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param> /// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
/// <returns>The user's ban history.</returns> /// <returns>The user's ban history.</returns>
Task<List<ServerBanDef>> GetServerBansAsync( Task<List<BanDef>> GetBansAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true); bool includeUnbanned=true,
BanType type = BanType.Server);
Task AddServerBanAsync(ServerBanDef serverBan); Task<BanDef> AddBanAsync(BanDef ban);
Task AddServerUnbanAsync(ServerUnbanDef serverBan); Task AddUnbanAsync(UnbanDef ban);
public Task EditServerBan( public Task EditBan(
int id, int id,
string reason, string reason,
NoteSeverity severity, NoteSeverity severity,
@@ -131,45 +133,6 @@ namespace Content.Server.Database
#endregion #endregion
#region Role Bans
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
/// </summary>
/// <param name="id">The role ban id to look for.</param>
/// <returns>The role ban with the given id or null if none exist.</returns>
Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
/// <summary>
/// Looks up an user's role ban history.
/// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
/// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
/// </summary>
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
public Task EditServerRoleBan(
int id,
string reason,
NoteSeverity severity,
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt);
#endregion
#region Playtime #region Playtime
/// <summary> /// <summary>
@@ -209,7 +172,7 @@ namespace Content.Server.Database
ConnectionDenyReason? denied, ConnectionDenyReason? denied,
int serverId); int serverId);
Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans); Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> bans);
#endregion #endregion
@@ -301,8 +264,7 @@ namespace Content.Server.Database
Task<AdminNoteRecord?> GetAdminNote(int id); Task<AdminNoteRecord?> GetAdminNote(int id);
Task<AdminWatchlistRecord?> GetAdminWatchlist(int id); Task<AdminWatchlistRecord?> GetAdminWatchlist(int id);
Task<AdminMessageRecord?> GetAdminMessage(int id); Task<AdminMessageRecord?> GetAdminMessage(int id);
Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id); Task<BanNoteRecord?> GetBanAsNoteAsync(int id);
Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id);
Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player); Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player); Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player);
Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player); Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player);
@@ -313,8 +275,7 @@ namespace Content.Server.Database
Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
/// <summary> /// <summary>
/// Mark an admin message as being seen by the target player. /// Mark an admin message as being seen by the target player.
@@ -522,49 +483,51 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetAssignedUserIdAsync(name)); return RunDbCommand(() => _db.GetAssignedUserIdAsync(name));
} }
public Task<ServerBanDef?> GetServerBanAsync(int id) public Task<BanDef?> GetBanAsync(int id)
{ {
DbReadOpsMetric.Inc(); DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(id)); return RunDbCommand(() => _db.GetBanAsync(id));
} }
public Task<ServerBanDef?> GetServerBanAsync( public Task<BanDef?> GetBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true) BanType type = BanType.Server)
{ {
DbReadOpsMetric.Inc(); DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); return RunDbCommand(() => _db.GetBanAsync(address, userId, hwId, modernHWIds, type));
} }
public Task AddServerBanAsync(ServerBanDef serverBan) public Task<List<BanDef>> GetBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true,
BanType type = BanType.Server)
{ {
DbWriteOpsMetric.Inc(); DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerBanAsync(serverBan)); return RunDbCommand(() => _db.GetBansAsync(address, userId, hwId, modernHWIds, includeUnbanned, type));
} }
public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) public Task<BanDef> AddBanAsync(BanDef ban)
{ {
DbWriteOpsMetric.Inc(); DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); return RunDbCommand(() => _db.AddBanAsync(ban));
} }
public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) public Task AddUnbanAsync(UnbanDef unban)
{ {
DbWriteOpsMetric.Inc(); DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); return RunDbCommand(() => _db.AddUnbanAsync(unban));
}
public Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditBan(id, reason, severity, expiration, editedBy, editedAt));
} }
public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags) public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
@@ -579,43 +542,6 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetBanExemption(userId, cancel)); return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
} }
#region Role Ban
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBanAsync(id));
}
public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan));
}
public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban));
}
public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt));
}
#endregion
#region Playtime #region Playtime
public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel) public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
@@ -667,7 +593,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId)); return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
} }
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans) public Task AddServerBanHitsAsync(int connection, IEnumerable<BanDef> bans)
{ {
DbWriteOpsMetric.Inc(); DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans)); return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans));
@@ -928,16 +854,10 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetAdminMessage(id)); return RunDbCommand(() => _db.GetAdminMessage(id));
} }
public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id) public Task<BanNoteRecord?> GetBanAsNoteAsync(int id)
{ {
DbReadOpsMetric.Inc(); DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); return RunDbCommand(() => _db.GetBanAsNoteAsync(id));
}
public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
} }
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player) public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
@@ -999,16 +919,10 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt));
} }
public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) public Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{ {
DbWriteOpsMetric.Inc(); DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); return RunDbCommand(() => _db.HideBanFromNotes(id, deletedBy, deletedAt));
}
public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt));
} }
public Task MarkMessageAsSeen(int id, bool dismissedToo) public Task MarkMessageAsSeen(int id, bool dismissedToo)

View File

@@ -62,24 +62,26 @@ namespace Content.Server.Database
} }
#region Ban #region Ban
public override async Task<ServerBanDef?> GetServerBanAsync(int id) public override async Task<BanDef?> GetBanAsync(int id)
{ {
await using var db = await GetDbImpl(); await using var db = await GetDbImpl();
var query = db.PgDbContext.Ban var query = db.PgDbContext.Ban
.Include(p => p.Unban) .ApplyIncludes(GetBanDefIncludes())
.Where(p => p.Id == id); .Where(p => p.Id == id)
.AsSplitQuery();
var ban = await query.SingleOrDefaultAsync(); var ban = await query.SingleOrDefaultAsync();
return ConvertBan(ban); return ConvertBan(ban);
} }
public override async Task<ServerBanDef?> GetServerBanAsync( public override async Task<BanDef?> GetBanAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds) ImmutableArray<ImmutableArray<byte>>? modernHWIds,
BanType type)
{ {
if (address == null && userId == null && hwId == null) if (address == null && userId == null && hwId == null)
{ {
@@ -90,7 +92,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId); var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); 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); .OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync(); var ban = await query.FirstOrDefaultAsync();
@@ -98,11 +100,12 @@ namespace Content.Server.Database
return ConvertBan(ban); return ConvertBan(ban);
} }
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address, public override async Task<List<BanDef>> GetBansAsync(IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned) bool includeUnbanned,
BanType type)
{ {
if (address == null && userId == null && hwId == null) if (address == null && userId == null && hwId == null)
{ {
@@ -111,12 +114,11 @@ namespace Content.Server.Database
await using var db = await GetDbImpl(); 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 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 queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length); var bans = new List<BanDef>(queryBans.Length);
foreach (var ban in queryBans) foreach (var ban in queryBans)
{ {
@@ -131,7 +133,8 @@ namespace Content.Server.Database
return bans; return bans;
} }
private static IQueryable<ServerBan> MakeBanLookupQuery( // This has to return IDs instead of direct objects because otherwise all the includes are too complicated.
private static IQueryable<Ban> MakeBanLookupQuery(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
@@ -139,34 +142,55 @@ namespace Content.Server.Database
DbGuardImpl db, DbGuardImpl db,
bool includeUnbanned, bool includeUnbanned,
ServerBanExemptFlags? exemptFlags, ServerBanExemptFlags? exemptFlags,
bool newPlayer) bool newPlayer,
BanType type)
{ {
DebugTools.Assert(!(address == null && userId == null && hwId == null)); DebugTools.Assert(!(address == null && userId == null && hwId == null));
var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>( var selectorQueries = new List<IQueryable<IBanSelector>>();
userId,
hwId,
modernHWIds,
db.PgDbContext.Ban);
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)) if (userId is { } uid)
selectorQueries.Add(db.DbContext.BanPlayer.Where(b => b.UserId == uid.UserId));
if (hwId != null && hwId.Value.Length > 0)
{ {
var newQ = db.PgDbContext.Ban selectorQueries.Add(db.DbContext.BanHwid.Where(bh =>
.Include(p => p.Unban) bh.HWId!.Type == HwidType.Legacy && bh.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray())
.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); if (modernHWIds != null)
{
foreach (var modernHwid in modernHWIds)
{
selectorQueries.Add(db.DbContext.BanHwid
.Where(b => b.HWId!.Type == HwidType.Modern
&& b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray())));
}
}
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None)
.HasFlag(ServerBanExemptFlags.IP))
{
selectorQueries.Add(db.PgDbContext.BanAddress
.Where(ba => EF.Functions.ContainsOrEqual(ba.Address, address)
&& !(ba.Ban!.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) &&
!newPlayer)));
} }
DebugTools.Assert( DebugTools.Assert(
query != null, selectorQueries.Count > 0,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null."); "At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
var selectorQuery = selectorQueries
.Select(q => q.Select(sel => sel.BanId))
.Aggregate((selectors, queryable) => selectors.Union(queryable));
var banQuery = db.DbContext.Ban.Where(b => selectorQuery.Contains(b.Id));
if (!includeUnbanned) if (!includeUnbanned)
{ {
query = query.Where(p => banQuery = banQuery.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
} }
@@ -175,68 +199,23 @@ namespace Content.Server.Database
if (exempt != ServerBanExemptFlags.None) if (exempt != ServerBanExemptFlags.None)
exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange
query = query.Where(b => (b.ExemptFlags & exempt) == 0); banQuery = banQuery.Where(b => (b.ExemptFlags & exempt) == 0);
} }
return query.Distinct(); return banQuery
.Where(b => b.Type == type)
.ApplyIncludes(GetBanDefIncludes(type))
.AsSplitQuery();
} }
private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>( [return: NotNullIfNotNull(nameof(ban))]
NetUserId? userId, private static BanDef? ConvertBan(Ban? ban)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbSet<TBan> set)
where TBan : class, IBanCommon<TUnban>
where TUnban : class, IUnbanCommon
{
IQueryable<TBan>? query = null;
if (userId is { } uid)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
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);
}
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);
}
}
return query;
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{ {
if (ban == null) if (ban == null)
{ {
return null; return null;
} }
NetUserId? uid = null;
if (ban.PlayerUserId is {} guid)
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null; NetUserId? aUid = null;
if (ban.BanningAdmin is {} aGuid) if (ban.BanningAdmin is {} aGuid)
{ {
@@ -245,23 +224,31 @@ namespace Content.Server.Database
var unbanDef = ConvertUnban(ban.Unban); var unbanDef = ConvertUnban(ban.Unban);
return new ServerBanDef( ImmutableArray<BanRoleDef>? roles = null;
if (ban.Type == BanType.Role)
{
roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))];
}
return new BanDef(
ban.Id, ban.Id,
uid, ban.Type,
ban.Address.ToTuple(), [..ban.Players!.Select(bp => new NetUserId(bp.UserId))],
ban.HWId, [..ban.Addresses!.Select(ba => ba.Address.ToTuple())],
[..ban.Hwids!.Select(bh => bh.HWId)],
ban.BanTime, ban.BanTime,
ban.ExpirationTime, ban.ExpirationTime,
ban.RoundId, [..ban.Rounds!.Select(r => r.RoundId)],
ban.PlaytimeAtNote, ban.PlaytimeAtNote,
ban.Reason, ban.Reason,
ban.Severity, ban.Severity,
aUid, aUid,
unbanDef, unbanDef,
ban.ExemptFlags); ban.ExemptFlags,
roles);
} }
private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) private static UnbanDef? ConvertUnban(Unban? unban)
{ {
if (unban == null) if (unban == null)
{ {
@@ -274,224 +261,54 @@ namespace Content.Server.Database
aUid = new NetUserId(aGuid); aUid = new NetUserId(aGuid);
} }
return new ServerUnbanDef( return new UnbanDef(
unban.Id, unban.Id,
aUid, aUid,
unban.UnbanTime); unban.UnbanTime);
} }
public override async Task AddServerBanAsync(ServerBanDef serverBan) public override async Task<BanDef> AddBanAsync(BanDef ban)
{ {
await using var db = await GetDbImpl(); await using var db = await GetDbImpl();
db.PgDbContext.Ban.Add(new ServerBan var banEntity = new Ban
{ {
Address = serverBan.Address.ToNpgsqlInet(), Type = ban.Type,
HWId = serverBan.HWId, Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })],
Reason = serverBan.Reason, Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })],
Severity = serverBan.Severity, Reason = ban.Reason,
BanningAdmin = serverBan.BanningAdmin?.UserId, Severity = ban.Severity,
BanTime = serverBan.BanTime.UtcDateTime, BanningAdmin = ban.BanningAdmin?.UserId,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, BanTime = ban.BanTime.UtcDateTime,
RoundId = serverBan.RoundId, ExpirationTime = ban.ExpirationTime?.UtcDateTime,
PlaytimeAtNote = serverBan.PlaytimeAtNote, Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })],
PlayerUserId = serverBan.UserId?.UserId, PlaytimeAtNote = ban.PlaytimeAtNote,
ExemptFlags = serverBan.ExemptFlags Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })],
}); ExemptFlags = ban.ExemptFlags,
Roles = ban.Roles == null
await db.PgDbContext.SaveChangesAsync(); ? []
} : ban.Roles.Value.Select(brd => new BanRole
{
public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) RoleType = brd.RoleType,
{ RoleId = brd.RoleId
await using var db = await GetDbImpl(); })
.ToList(),
db.PgDbContext.Unban.Add(new ServerUnban
{
BanId = serverUnban.BanId,
UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
UnbanTime = serverUnban.UnbanTime.UtcDateTime
});
await db.PgDbContext.SaveChangesAsync();
}
#endregion
#region Role Ban
public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
await using var db = await GetDbImpl();
var query = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(p => p.Id == id);
var ban = await query.SingleOrDefaultAsync();
return ConvertRoleBan(ban);
}
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
}
private static async Task<List<ServerRoleBanDef>> QueryRoleBans(IQueryable<ServerRoleBan> query)
{
var queryRoleBans = await query.ToArrayAsync();
var bans = new List<ServerRoleBanDef>(queryRoleBans.Length);
foreach (var ban in queryRoleBans)
{
var banDef = ConvertRoleBan(ban);
if (banDef != null)
{
bans.Add(banDef);
}
}
return bans;
}
private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned)
{
var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.RoleBan);
if (address != null)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
}
query = query!.Distinct();
return query;
}
[return: NotNullIfNotNull(nameof(ban))]
private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? 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);
return new ServerRoleBanDef(
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
aUid,
unbanDef,
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,
unban.UnbanTime);
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
{
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,
}; };
db.PgDbContext.RoleBan.Add(ban); db.PgDbContext.Ban.Add(banEntity);
await db.PgDbContext.SaveChangesAsync(); 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(); await using var db = await GetDbImpl();
db.PgDbContext.RoleUnban.Add(new ServerRoleUnban db.PgDbContext.Unban.Add(new Unban
{ {
BanId = serverRoleUnban.BanId, BanId = unban.BanId,
UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId, UnbanningAdmin = unban.UnbanningAdmin?.UserId,
UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime UnbanTime = unban.UnbanTime.UtcDateTime
}); });
await db.PgDbContext.SaveChangesAsync(); await db.PgDbContext.SaveChangesAsync();

View File

@@ -70,48 +70,52 @@ namespace Content.Server.Database
} }
#region Ban #region Ban
public override async Task<ServerBanDef?> GetServerBanAsync(int id) public override async Task<BanDef?> GetBanAsync(int id)
{ {
await using var db = await GetDbImpl(); await using var db = await GetDbImpl();
var ban = await db.SqliteDbContext.Ban var ban = await db.SqliteDbContext.Ban
.Include(p => p.Unban) .ApplyIncludes(GetBanDefIncludes())
.Where(p => p.Id == id) .Where(p => p.Id == id)
.AsSplitQuery()
.SingleOrDefaultAsync(); .SingleOrDefaultAsync();
return ConvertBan(ban); return ConvertBan(ban);
} }
public override async Task<ServerBanDef?> GetServerBanAsync( public override async Task<BanDef?> GetBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned) BanType type)
{ {
await using var db = await GetDbImpl(); 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: false, type)).FirstOrDefault();
} }
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync( public override async Task<List<BanDef>> GetBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned,
BanType type)
{
await using var db = await GetDbImpl();
return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned, type)).ToList();
}
private async Task<IEnumerable<BanDef>> GetBanQueryAsync(
DbGuardImpl db, DbGuardImpl db,
IPAddress? address, IPAddress? address,
NetUserId? userId, NetUserId? userId,
ImmutableArray<byte>? hwId, ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds, ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned) bool includeUnbanned,
BanType type)
{ {
var exempt = await GetBanExemptionCore(db, userId); var exempt = await GetBanExemptionCore(db, userId);
@@ -119,7 +123,7 @@ namespace Content.Server.Database
// SQLite can't do the net masking stuff we need to match IP address ranges. // SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory. // 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 var playerInfo = new BanMatcher.PlayerInfo
{ {
@@ -136,12 +140,12 @@ namespace Content.Server.Database
.Where(b => BanMatcher.BanMatches(b!, playerInfo))!; .Where(b => BanMatcher.BanMatches(b!, playerInfo))!;
} }
private static async Task<List<ServerBan>> GetAllBans( private static async Task<List<Ban>> GetAllBans(SqliteServerDbContext db,
SqliteServerDbContext db,
bool includeUnbanned, bool includeUnbanned,
ServerBanExemptFlags? exemptFlags) ServerBanExemptFlags? exemptFlags,
BanType type)
{ {
IQueryable<ServerBan> query = db.Ban.Include(p => p.Unban); var query = db.Ban.Where(b => b.Type == type).ApplyIncludes(GetBanDefIncludes(type));
if (!includeUnbanned) if (!includeUnbanned)
{ {
query = query.Where(p => query = query.Where(p =>
@@ -157,244 +161,65 @@ namespace Content.Server.Database
query = query.Where(b => (b.ExemptFlags & exempt) == 0); query = query.Where(b => (b.ExemptFlags & exempt) == 0);
} }
return await query.ToListAsync(); return await query.AsSplitQuery().ToListAsync();
} }
public override async Task AddServerBanAsync(ServerBanDef serverBan) public override async Task<BanDef> AddBanAsync(BanDef ban)
{ {
await using var db = await GetDbImpl(); await using var db = await GetDbImpl();
db.SqliteDbContext.Ban.Add(new ServerBan var banEntity = new Ban
{ {
Address = serverBan.Address.ToNpgsqlInet(), Type = ban.Type,
Reason = serverBan.Reason, Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })],
Severity = serverBan.Severity, Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })],
BanningAdmin = serverBan.BanningAdmin?.UserId, Reason = ban.Reason,
HWId = serverBan.HWId, Severity = ban.Severity,
BanTime = serverBan.BanTime.UtcDateTime, BanningAdmin = ban.BanningAdmin?.UserId,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, BanTime = ban.BanTime.UtcDateTime,
RoundId = serverBan.RoundId, ExpirationTime = ban.ExpirationTime?.UtcDateTime,
PlaytimeAtNote = serverBan.PlaytimeAtNote, Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })],
PlayerUserId = serverBan.UserId?.UserId, PlaytimeAtNote = ban.PlaytimeAtNote,
ExemptFlags = serverBan.ExemptFlags Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })],
}); ExemptFlags = ban.ExemptFlags,
Roles = ban.Roles == null
await db.SqliteDbContext.SaveChangesAsync(); ? []
} : ban.Roles.Value.Select(brd => new BanRole
public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
{
await using var db = await GetDbImpl();
db.SqliteDbContext.Unban.Add(new ServerUnban
{
BanId = serverUnban.BanId,
UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
UnbanTime = serverUnban.UnbanTime.UtcDateTime
});
await db.SqliteDbContext.SaveChangesAsync();
}
#endregion
#region Role Ban
public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
await using var db = await GetDbImpl();
var ban = await db.SqliteDbContext.RoleBan
.Include(p => p.Unban)
.Where(p => p.Id == id)
.SingleOrDefaultAsync();
return ConvertRoleBan(ban);
}
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
.Select(ConvertRoleBan)
.ToList()!;
}
private static async Task<List<ServerRoleBan>> GetAllRoleBans(
SqliteServerDbContext db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan> query = db.RoleBan.Include(p => p.Unban);
if (!includeUnbanned)
{
query = query.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
}
return await query.ToListAsync();
}
private static bool RoleBanMatches(
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
{
return true;
}
if (userId is { } id && ban.PlayerUserId == id.UserId)
{
return true;
}
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
break;
case HwidType.Modern:
if (modernHWIds != null)
{
foreach (var modernHWId in modernHWIds)
{ {
if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid)) RoleType = brd.RoleType,
return true; RoleId = brd.RoleId
} })
} .ToList(),
break;
}
return false;
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
{
await using var db = await GetDbImpl();
var ban = new ServerRoleBan
{
Address = serverBan.Address.ToNpgsqlInet(),
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
PlaytimeAtNote = serverBan.PlaytimeAtNote,
PlayerUserId = serverBan.UserId?.UserId,
RoleId = serverBan.Role,
}; };
db.SqliteDbContext.RoleBan.Add(ban); db.SqliteDbContext.Ban.Add(banEntity);
await db.SqliteDbContext.SaveChangesAsync(); 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(); await using var db = await GetDbImpl();
db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban db.SqliteDbContext.Unban.Add(new Unban
{ {
BanId = serverUnban.BanId, BanId = unban.BanId,
UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, UnbanningAdmin = unban.UnbanningAdmin?.UserId,
UnbanTime = serverUnban.UnbanTime.UtcDateTime UnbanTime = unban.UnbanTime.UtcDateTime
}); });
await db.SqliteDbContext.SaveChangesAsync(); await db.SqliteDbContext.SaveChangesAsync();
} }
[return: NotNullIfNotNull(nameof(ban))]
private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? 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 #endregion
[return: NotNullIfNotNull(nameof(ban))] [return: NotNullIfNotNull(nameof(ban))]
private static ServerBanDef? ConvertBan(ServerBan? ban) private static BanDef? ConvertBan(Ban? ban)
{ {
if (ban == null) if (ban == null)
{ {
return null; return null;
} }
NetUserId? uid = null;
if (ban.PlayerUserId is { } guid)
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null; NetUserId? aUid = null;
if (ban.BanningAdmin is { } aGuid) if (ban.BanningAdmin is { } aGuid)
{ {
@@ -403,23 +228,32 @@ namespace Content.Server.Database
var unban = ConvertUnban(ban.Unban); var unban = ConvertUnban(ban.Unban);
return new ServerBanDef( ImmutableArray<BanRoleDef>? roles = null;
if (ban.Type == BanType.Role)
{
roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))];
}
return new BanDef(
ban.Id, ban.Id,
uid, ban.Type,
ban.Address.ToTuple(), [..ban.Players!.Select(bp => new NetUserId(bp.UserId))],
ban.HWId, [..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. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
ban.RoundId, [..ban.Rounds!.Select(r => r.RoundId)],
ban.PlaytimeAtNote, ban.PlaytimeAtNote,
ban.Reason, ban.Reason,
ban.Severity, ban.Severity,
aUid, aUid,
unban); unban,
ban.ExemptFlags,
roles);
} }
private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) private static UnbanDef? ConvertUnban(Unban? unban)
{ {
if (unban == null) if (unban == null)
{ {
@@ -432,7 +266,7 @@ namespace Content.Server.Database
aUid = new NetUserId(aGuid); aUid = new NetUserId(aGuid);
} }
return new ServerUnbanDef( return new UnbanDef(
unban.Id, unban.Id,
aUid, aUid,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC. // SQLite apparently always reads DateTime as unspecified, but we always write as UTC.

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -2,7 +2,7 @@
namespace Content.Server.Database namespace Content.Server.Database
{ {
public sealed class ServerUnbanDef public sealed class UnbanDef
{ {
public int BanId { get; } public int BanId { get; }
@@ -10,7 +10,7 @@ namespace Content.Server.Database
public DateTimeOffset UnbanTime { get; } public DateTimeOffset UnbanTime { get; }
public ServerUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) public UnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
{ {
BanId = banId; BanId = banId;
UnbanningAdmin = unbanningAdmin; UnbanningAdmin = unbanningAdmin;

View File

@@ -11,22 +11,14 @@ namespace Content.Server.IP
{ {
// Npgsql used to map inet types as a tuple like this. // 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. // 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 new NpgsqlInet(tuple.Item1, (byte)tuple.Item2);
return null;
return new NpgsqlInet(tuple.Value.Item1, (byte) tuple.Value.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 (inet.Address, inet.Netmask);
return null;
return (inet.Value.Address, inet.Value.Netmask);
} }
// Taken from https://stackoverflow.com/a/56461160/4678631 // Taken from https://stackoverflow.com/a/56461160/4678631

View File

@@ -371,14 +371,7 @@ namespace Content.Server.Voting.Managers
} }
var targetUid = located.UserId; var targetUid = located.UserId;
var targetHWid = located.LastHWId; var targetHWid = located.LastHWId;
(IPAddress, int)? targetIP = null; var targetIP = located.LastAddress;
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.
}
if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession)) if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession))
{ {
@@ -544,7 +537,15 @@ namespace Content.Server.Voting.Managers
uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration); 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 else

View File

@@ -0,0 +1,33 @@
namespace Content.Shared.Database;
/// <summary>
/// Types of bans that can be stored in the database.
/// </summary>
public enum BanType : byte
{
/// <summary>
/// A ban from the entire server. If a player matches the ban info, they will be refused connection.
/// </summary>
Server,
/// <summary>
/// A ban from playing one or more roles.
/// </summary>
Role,
}
/// <summary>
/// A single role for a database role ban.
/// </summary>
/// <param name="RoleType">The type of role being banned, e.g. <c>Job</c>.</param>
/// <param name="RoleId">
/// The ID of the role being banned. This is likely a prototype ID based on <paramref name="RoleType"/>.
/// </param>
[Serializable]
public record struct BanRoleDef(string RoleType, string RoleId)
{
public override string ToString()
{
return $"{RoleType}:{RoleId}";
}
}

View File

@@ -6,7 +6,7 @@ namespace Content.Shared.Administration.BanList;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class BanListEuiState : EuiStateBase public sealed class BanListEuiState : EuiStateBase
{ {
public BanListEuiState(string banListPlayerName, List<SharedServerBan> bans, List<SharedServerRoleBan> roleBans) public BanListEuiState(string banListPlayerName, List<SharedBan> bans, List<SharedBan> roleBans)
{ {
BanListPlayerName = banListPlayerName; BanListPlayerName = banListPlayerName;
Bans = bans; Bans = bans;
@@ -14,6 +14,6 @@ public sealed class BanListEuiState : EuiStateBase
} }
public string BanListPlayerName { get; } public string BanListPlayerName { get; }
public List<SharedServerBan> Bans { get; } public List<SharedBan> Bans { get; }
public List<SharedServerRoleBan> RoleBans { get; } public List<SharedBan> RoleBans { get; }
} }

View File

@@ -0,0 +1,20 @@
using System.Collections.Immutable;
using Content.Shared.Database;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.Administration.BanList;
[Serializable, NetSerializable]
public record SharedBan(
int? Id,
BanType Type,
ImmutableArray<NetUserId> UserIds,
ImmutableArray<(string address, int cidrMask)> Addresses,
ImmutableArray<string> HWIds,
DateTime BanTime,
DateTime? ExpirationTime,
string Reason,
string? BanningAdminName,
SharedUnban? Unban,
ImmutableArray<BanRoleDef>? Roles);

View File

@@ -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
);

View File

@@ -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);

View File

@@ -3,7 +3,7 @@
namespace Content.Shared.Administration.BanList; namespace Content.Shared.Administration.BanList;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed record SharedServerUnban( public sealed record SharedUnban(
string? UnbanningAdmin, string? UnbanningAdmin,
DateTime UnbanTime DateTime UnbanTime
); );

View File

@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using Content.Shared.Database; using Content.Shared.Database;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -7,8 +8,8 @@ namespace Content.Shared.Administration.Notes;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed record SharedAdminNote( 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. 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 ImmutableArray<NetUserId> Players, // Notes player
int? Round, // Which round was it added in? ImmutableArray<int> Rounds, // Which round was it added in?
string? ServerName, // Which server was this added on? string? ServerName, // Which server was this added on?
TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note
NoteType NoteType, // Type of note NoteType NoteType, // Type of note
@@ -20,7 +21,7 @@ public sealed record SharedAdminNote(
DateTime CreatedAt, // When was it created? DateTime CreatedAt, // When was it created?
DateTime? LastEditedAt, // When was it last edited? DateTime? LastEditedAt, // When was it last edited?
DateTime? ExpiryTime, // Does it expire? DateTime? ExpiryTime, // Does it expire?
string[]? BannedRoles, // Only valid for role bans. List of banned roles ImmutableArray<BanRoleDef>? BannedRoles, // Only valid for role bans. List of banned roles
DateTime? UnbannedTime, // Only valid for bans. Set if unbanned DateTime? UnbannedTime, // Only valid for bans. Set if unbanned
string? UnbannedByName, // 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? bool? Seen // Only valid for messages, otherwise should be null. Has the user seen this message?

View File

@@ -1,5 +1,7 @@
using Content.Shared.Roles;
using Lidgren.Network; using Lidgren.Network;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Players; namespace Content.Shared.Players;
@@ -11,8 +13,8 @@ public sealed class MsgRoleBans : NetMessage
{ {
public override MsgGroups MsgGroup => MsgGroups.EntityEvent; public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
public List<string> JobBans = new(); public List<ProtoId<JobPrototype>> JobBans = new();
public List<string> AntagBans = new(); public List<ProtoId<AntagPrototype>> AntagBans = new();
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{ {

View File

@@ -44,7 +44,6 @@ cmd-roleban-severity-parse = ${severity} is not a valid severity\n{$help}.
cmd-roleban-arg-count = Invalid amount of arguments. cmd-roleban-arg-count = Invalid amount of arguments.
cmd-roleban-job-parse = Job {$job} does not exist. cmd-roleban-job-parse = Job {$job} does not exist.
cmd-roleban-name-parse = Unable to find a player with that name. 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-success = Role banned {$target} from {$role} with reason {$reason} {$length}.
cmd-roleban-inf = permanently cmd-roleban-inf = permanently

View File

@@ -131,6 +131,7 @@
- fuck - fuck
- replay_recording_start - replay_recording_start
- replay_recording_stop - replay_recording_stop
- transfer_test
- Flags: QUERY - Flags: QUERY
Commands: Commands:

View File

@@ -8,7 +8,7 @@ import os
import psycopg2 import psycopg2
from uuid import UUID from uuid import UUID
LATEST_DB_MIGRATION = "20250314222016_ConstructionFavorites" LATEST_DB_MIGRATION = "20260120200503_BanRefactor"
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -42,9 +42,8 @@ def main():
dump_player(cur, user_id, arg_output) dump_player(cur, user_id, arg_output)
dump_preference(cur, user_id, arg_output) dump_preference(cur, user_id, arg_output)
dump_role_whitelists(cur, user_id, arg_output) dump_role_whitelists(cur, user_id, arg_output)
dump_server_ban(cur, user_id, arg_output) dump_ban(cur, user_id, arg_output)
dump_server_ban_exemption(cur, user_id, arg_output) dump_server_ban_exemption(cur, user_id, arg_output)
dump_server_role_ban(cur, user_id, arg_output)
dump_uploaded_resource_log(cur, user_id, arg_output) dump_uploaded_resource_log(cur, user_id, arg_output)
dump_whitelist(cur, user_id, arg_output) dump_whitelist(cur, user_id, arg_output)
@@ -301,7 +300,7 @@ FROM (
f.write(json_data) f.write(json_data)
def dump_server_ban(cur: "psycopg2.cursor", user_id: str, outdir: str): def dump_ban(cur: "psycopg2.cursor", user_id: str, outdir: str):
print("Dumping server_ban...") print("Dumping server_ban...")
cur.execute(""" cur.execute("""
@@ -311,19 +310,39 @@ FROM (
SELECT SELECT
*, *,
(SELECT to_jsonb(unban_sq) - 'ban_id' FROM ( (SELECT to_jsonb(unban_sq) - 'ban_id' FROM (
SELECT * FROM server_unban WHERE server_unban.ban_id = server_ban.server_ban_id SELECT * FROM unban WHERE unban.ban_id = ban.ban_id
) unban_sq) ) unban_sq)
as unban as unban,
(SELECT COALESCE(json_agg(to_jsonb(ban_player_subq) - 'ban_id'), '[]') FROM (
SELECT * FROM ban_player WHERE ban_player.ban_id = ban.ban_id
) ban_player_subq)
as ban_player,
(SELECT COALESCE(json_agg(to_jsonb(ban_address_subq) - 'ban_id'), '[]') FROM (
SELECT * FROM ban_address WHERE ban_address.ban_id = ban.ban_id
) ban_address_subq)
as ban_address,
(SELECT COALESCE(json_agg(to_jsonb(ban_role_subq) - 'ban_id'), '[]') FROM (
SELECT * FROM ban_role WHERE ban_role.ban_id = ban.ban_id
) ban_role_subq)
as ban_role,
(SELECT COALESCE(json_agg(to_jsonb(ban_hwid_subq) - 'ban_id'), '[]') FROM (
SELECT * FROM ban_hwid WHERE ban_hwid.ban_id = ban.ban_id
) ban_hwid_subq)
as ban_hwid,
(SELECT COALESCE(json_agg(to_jsonb(ban_round_subq) - 'ban_id'), '[]') FROM (
SELECT * FROM ban_round WHERE ban_round.ban_id = ban.ban_id
) ban_round_subq)
as ban_round
FROM FROM
server_ban ban
WHERE WHERE
player_user_id = %s ban_id IN (SELECT bp.ban_id FROM ban_player bp WHERE bp.user_id = %s)
) as data ) as data
""", (user_id,)) """, (user_id,))
json_data = cur.fetchall()[0][0] json_data = cur.fetchall()[0][0]
with open(os.path.join(outdir, "server_ban.json"), "w", encoding="utf-8") as f: with open(os.path.join(outdir, "ban.json"), "w", encoding="utf-8") as f:
f.write(json_data) f.write(json_data)

View File

@@ -12,7 +12,7 @@ import os
import psycopg2 import psycopg2
from uuid import UUID from uuid import UUID
LATEST_DB_MIGRATION = "20250314222016_ConstructionFavorites" LATEST_DB_MIGRATION = "20260120200503_BanRefactor"
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -38,9 +38,8 @@ def main():
clear_play_time(cur, user_id) clear_play_time(cur, user_id)
clear_player(cur, user_id) clear_player(cur, user_id)
clear_preference(cur, user_id) clear_preference(cur, user_id)
clear_server_ban(cur, user_id) clear_ban(cur, user_id)
clear_server_ban_exemption(cur, user_id) clear_server_ban_exemption(cur, user_id)
clear_server_role_ban(cur, user_id)
clear_uploaded_resource_log(cur, user_id) clear_uploaded_resource_log(cur, user_id)
clear_whitelist(cur, user_id) clear_whitelist(cur, user_id)
clear_blacklist(cur, user_id) clear_blacklist(cur, user_id)
@@ -144,14 +143,14 @@ WHERE
""", (user_id,)) """, (user_id,))
def clear_server_ban(cur: "psycopg2.cursor", user_id: str): def clear_ban(cur: "psycopg2.cursor", user_id: str):
print("Clearing server_ban...") print("Clearing ban...")
cur.execute(""" cur.execute("""
DELETE FROM DELETE FROM
server_ban ban
WHERE WHERE
player_user_id = %s ban_id IN (SELECT bp.ban_id FROM ban_player bp WHERE bp.user_id = %s)
""", (user_id,)) """, (user_id,))
@@ -166,17 +165,6 @@ WHERE
""", (user_id,)) """, (user_id,))
def clear_server_role_ban(cur: "psycopg2.cursor", user_id: str):
print("Clearing server_role_ban...")
cur.execute("""
DELETE FROM
server_role_ban
WHERE
player_user_id = %s
""", (user_id,))
def clear_uploaded_resource_log(cur: "psycopg2.cursor", user_id: str): def clear_uploaded_resource_log(cur: "psycopg2.cursor", user_id: str):
print("Clearing uploaded_resource_log...") print("Clearing uploaded_resource_log...")