From 2e6eaa45c57c7f5ba561d1fb1ef6712d2432a8fa Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 20 Feb 2024 10:13:31 +0100 Subject: [PATCH] Fix admin notes and database time nonsense. (#25280) God bloody christ. There's like three layers of shit here. So firstly, apparently we were still using Npgsql.EnableLegacyTimestampBehavior. This means that time values (which are stored UTC in the database) were converted to local time when read out. This meant they were passed around as kind Local to clients (instead of UTC in the case of SQLite). That's easy enough to fix just turn off the flag and fix the couple spots we're passing a local DateTime ez. Oh but it turns out there's a DIFFERENT problem with SQLite: See SQLite we definitely store the DateTimes as UTC, but when Microsoft.Data.Sqlite reads them it reads them as Kind Unspecified instead of Utc. Why are these so bad? Because the admin notes system passes DateTime instances from EF Core straight to the rest of the game code. And that means it's a PAIN IN THE ASS to run the necessary conversions to fix the DateTime instances. GOD DAMNIT now I have to make a whole new set of "Record" entities so we avoid leaking the EF Core model entities. WAAAAAAA. Fixes #19897 --- .../UI/Notes/AdminNotesLine.xaml.cs | 6 +- .../UI/Notes/AdminNotesLinePopup.xaml.cs | 6 +- .../Administration/UI/Notes/NoteEdit.xaml.cs | 6 +- Content.Server.Database/Model.cs | 31 +- Content.Server.Database/ModelPostgres.cs | 5 - .../Administration/Notes/AdminMessageEui.cs | 6 +- .../Notes/AdminNotesExtensions.cs | 28 +- .../Administration/Notes/AdminNotesManager.cs | 12 +- .../Notes/IAdminNotesManager.cs | 8 +- Content.Server/Database/DatabaseRecords.cs | 127 ++++++ Content.Server/Database/PlayerRecord.cs | 32 -- Content.Server/Database/ServerBanNote.cs | 14 - Content.Server/Database/ServerDbBase.cs | 366 ++++++++++++------ Content.Server/Database/ServerDbManager.cs | 122 +++--- Content.Server/Database/ServerDbPostgres.cs | 21 +- Content.Server/Database/ServerDbSqlite.cs | 18 +- Content.Server/Database/ServerRoleBanNote.cs | 14 - .../Administration/Notes/SharedAdminNote.cs | 3 +- Content.Shared/CCVar/CCVars.cs | 2 +- 19 files changed, 501 insertions(+), 326 deletions(-) create mode 100644 Content.Server/Database/DatabaseRecords.cs delete mode 100644 Content.Server/Database/PlayerRecord.cs delete mode 100644 Content.Server/Database/ServerBanNote.cs delete mode 100644 Content.Server/Database/ServerRoleBanNote.cs diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs index e70248880b..ead1d8b00e 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs @@ -68,7 +68,7 @@ private void Refresh() SeverityRect.Texture = _sprites.Frame0(new SpriteSpecifier.Texture(new ResPath(iconPath))); } - TimeLabel.Text = Note.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss"); + TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); ServerLabel.Text = Note.ServerName ?? "Unknown"; RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round; AdminLabel.Text = Note.CreatedByName; @@ -91,7 +91,7 @@ private void Refresh() if (Note.ExpiryTime.Value > DateTime.UtcNow) { ExpiresLabel.Text = Loc.GetString("admin-note-editor-expiry-label-params", - ("date", Note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss")), + ("date", Note.ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")), ("expiresIn", (Note.ExpiryTime.Value - DateTime.UtcNow).ToString("d'd 'hh':'mm"))); ExpiresLabel.Modulate = Color.FromHex("#86DC3D"); } @@ -104,7 +104,7 @@ private void Refresh() if (Note.LastEditedAt > Note.CreatedAt) { - EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt)); + EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt.Value.ToLocalTime())); EditedLabel.Visible = true; } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs index 5ef29513e2..18a5003158 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs @@ -36,12 +36,12 @@ public AdminNotesLinePopup(SharedAdminNote note, string playerName, bool showDel ? Loc.GetString("admin-notes-round-id-unknown") : Loc.GetString("admin-notes-round-id", ("id", note.Round)); CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName)); - CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.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)); - EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? Loc.GetString("admin-notes-edited-never"))); + EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? Loc.GetString("admin-notes-edited-never"))); ExpiryTimeLabel.Text = note.ExpiryTime == null ? Loc.GetString("admin-notes-expires-never") - : Loc.GetString("admin-notes-expires", ("expires", note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss"))); + : Loc.GetString("admin-notes-expires", ("expires", note.ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); NoteTextEdit.InsertAtCursor(note.Message); if (note.NoteType is NoteType.ServerBan or NoteType.RoleBan) diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs index 77dde4688d..6f314f7954 100644 --- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs +++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs @@ -81,7 +81,7 @@ public NoteEdit(SharedAdminNote? note, string playerName, bool canCreate, bool c { PermanentCheckBox.Pressed = false; UpdatePermanentCheckboxFields(); - ExpiryLineEdit.Text = ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss"); + ExpiryLineEdit.Text = ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); } } @@ -173,7 +173,7 @@ private void UpdatePermanentCheckboxFields() ExpiryLabel.Visible = !PermanentCheckBox.Pressed; ExpiryLineEdit.Visible = !PermanentCheckBox.Pressed; - ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty; + ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty; } private void OnSecretPressed(BaseButton.ButtonEventArgs _) @@ -269,7 +269,7 @@ private bool ParseExpiryTime() return false; } - ExpiryTime = result; + ExpiryTime = result.ToUniversalTime(); ExpiryLineEdit.ModulateSelfOverride = null; return true; } diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index d6dec1dc3e..f9ec7811f5 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -874,33 +874,8 @@ public sealed class UploadedResourceLog public byte[] Data { get; set; } = default!; } - public interface IAdminRemarksCommon - { - public int Id { get; } - - public int? RoundId { get; } - public Round? Round { get; } - - public Guid? PlayerUserId { get; } - public Player? Player { get; } - public TimeSpan PlaytimeAtNote { get; } - - public string Message { get; } - - public Player? CreatedBy { get; } - - public DateTime CreatedAt { get; } - - public Player? LastEditedBy { get; } - - public DateTime? LastEditedAt { get; } - public DateTime? ExpirationTime { get; } - - public bool Deleted { get; } - } - [Index(nameof(PlayerUserId))] - public class AdminNote : IAdminRemarksCommon + public class AdminNote { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } @@ -934,7 +909,7 @@ public class AdminNote : IAdminRemarksCommon } [Index(nameof(PlayerUserId))] - public class AdminWatchlist : IAdminRemarksCommon + public class AdminWatchlist { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } @@ -965,7 +940,7 @@ public class AdminWatchlist : IAdminRemarksCommon } [Index(nameof(PlayerUserId))] - public class AdminMessage : IAdminRemarksCommon + public class AdminMessage { [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index a6b1856ab1..7499d0b0f5 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -10,11 +10,6 @@ namespace Content.Server.Database { public sealed class PostgresServerDbContext : ServerDbContext { - static PostgresServerDbContext() - { - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); - } - public PostgresServerDbContext(DbContextOptions options) : base(options) { } diff --git a/Content.Server/Administration/Notes/AdminMessageEui.cs b/Content.Server/Administration/Notes/AdminMessageEui.cs index ddb91aca7c..c5e0b60172 100644 --- a/Content.Server/Administration/Notes/AdminMessageEui.cs +++ b/Content.Server/Administration/Notes/AdminMessageEui.cs @@ -13,7 +13,7 @@ public sealed class AdminMessageEui : BaseEui [Dependency] private readonly IAdminNotesManager _notesMan = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; private readonly float _closeWait; - private AdminMessage? _message; + private AdminMessageRecord? _message; private DateTime _startTime; public AdminMessageEui() @@ -22,7 +22,7 @@ public AdminMessageEui() _closeWait = _cfg.GetCVar(CCVars.MessageWaitTime); } - public void SetMessage(AdminMessage message) + public void SetMessage(AdminMessageRecord message) { _message = message; _startTime = DateTime.UtcNow; @@ -37,7 +37,7 @@ public override EuiStateBase GetNewState() _closeWait, _message.Message, _message.CreatedBy?.LastSeenUserName ?? "[System]", - _message.CreatedAt + _message.CreatedAt.UtcDateTime ); } diff --git a/Content.Server/Administration/Notes/AdminNotesExtensions.cs b/Content.Server/Administration/Notes/AdminNotesExtensions.cs index 44ad20eec6..349c7ff3bd 100644 --- a/Content.Server/Administration/Notes/AdminNotesExtensions.cs +++ b/Content.Server/Administration/Notes/AdminNotesExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; @@ -7,7 +6,7 @@ namespace Content.Server.Administration.Notes; public static class AdminNotesExtensions { - public static SharedAdminNote ToShared(this IAdminRemarksCommon note) + public static SharedAdminNote ToShared(this IAdminRemarksRecord note) { NoteSeverity? severity = null; var secret = false; @@ -18,26 +17,26 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) bool? seen = null; switch (note) { - case AdminNote adminNote: + case AdminNoteRecord adminNote: type = NoteType.Note; severity = adminNote.Severity; secret = adminNote.Secret; break; - case AdminWatchlist: + case AdminWatchlistRecord: type = NoteType.Watchlist; secret = true; break; - case AdminMessage adminMessage: + case AdminMessageRecord adminMessage: type = NoteType.Message; seen = adminMessage.Seen; break; - case ServerBanNote ban: + case ServerBanNoteRecord ban: type = NoteType.ServerBan; severity = ban.Severity; unbannedTime = ban.UnbanTime; unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user"); break; - case ServerRoleBanNote roleBan: + case ServerRoleBanNoteRecord roleBan: type = NoteType.RoleBan; severity = roleBan.Severity; bannedRoles = roleBan.Roles; @@ -49,12 +48,13 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) } // There may be bans without a user, but why would we ever be converting them to shared notes? - if (note.PlayerUserId is null) - throw new ArgumentNullException(nameof(note.PlayerUserId), "Player user ID cannot be null for a note"); + if (note.Player is null) + throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); + return new SharedAdminNote( note.Id, - note.PlayerUserId.Value, - note.RoundId, + note.Player!.UserId, + note.Round?.Id, note.Round?.Server.Name, note.PlaytimeAtNote, type, @@ -63,9 +63,9 @@ public static SharedAdminNote ToShared(this IAdminRemarksCommon note) secret, note.CreatedBy?.LastSeenUserName ?? Loc.GetString("system-user"), note.LastEditedBy?.LastSeenUserName ?? string.Empty, - note.CreatedAt, - note.LastEditedAt, - note.ExpirationTime, + note.CreatedAt.UtcDateTime, + note.LastEditedAt?.UtcDateTime, + note.ExpirationTime?.UtcDateTime, bannedRoles, unbannedTime, unbannedByName, diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 0c1e7f3daa..e09e190648 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -144,7 +144,7 @@ public async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType var note = new SharedAdminNote( noteId, - player, + (NetUserId) player, roundId, serverName, playtime, @@ -306,27 +306,27 @@ public async Task ModifyAdminRemark(int noteId, NoteType type, ICommonSession ed NoteModified?.Invoke(newNote); } - public async Task> GetAllAdminRemarks(Guid player) + public async Task> GetAllAdminRemarks(Guid player) { return await _db.GetAllAdminRemarks(player); } - public async Task> GetVisibleRemarks(Guid player) + public async Task> GetVisibleRemarks(Guid player) { if (_config.GetCVar(CCVars.SeeOwnNotes)) { return await _db.GetVisibleAdminNotes(player); } _sawmill.Warning($"Someone tried to call GetVisibleNotes for {player} when see_own_notes was false"); - return new List(); + return new List(); } - public async Task> GetActiveWatchlists(Guid player) + public async Task> GetActiveWatchlists(Guid player) { return await _db.GetActiveWatchlists(player); } - public async Task> GetNewMessages(Guid player) + public async Task> GetNewMessages(Guid player) { return await _db.GetMessages(player); } diff --git a/Content.Server/Administration/Notes/IAdminNotesManager.cs b/Content.Server/Administration/Notes/IAdminNotesManager.cs index a726bd11c8..81ebd3e716 100644 --- a/Content.Server/Administration/Notes/IAdminNotesManager.cs +++ b/Content.Server/Administration/Notes/IAdminNotesManager.cs @@ -26,24 +26,24 @@ public interface IAdminNotesManager /// /// Desired player's /// ALL non-deleted notes, secret or not - Task> GetAllAdminRemarks(Guid player); + Task> GetAllAdminRemarks(Guid player); /// /// Queries the database and retrieves the notes a player should see /// /// Desired player's /// All player-visible notes - Task> GetVisibleRemarks(Guid player); + Task> GetVisibleRemarks(Guid player); /// /// Queries the database and retrieves watchlists that may have been placed on the player /// /// Desired player's /// Active watchlists - Task> GetActiveWatchlists(Guid player); + Task> GetActiveWatchlists(Guid player); /// /// Queries the database and retrieves new messages a player has gotten /// /// Desired player's /// All unread messages - Task> GetNewMessages(Guid player); + Task> GetNewMessages(Guid player); Task MarkMessageAsSeen(int id); } diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs new file mode 100644 index 0000000000..af740a4d74 --- /dev/null +++ b/Content.Server/Database/DatabaseRecords.cs @@ -0,0 +1,127 @@ +using System.Collections.Immutable; +using System.Net; +using Content.Shared.Database; +using Robust.Shared.Network; + +namespace Content.Server.Database; + +// This file contains copies of records returned from the database. +// We can't return the raw EF Core entities as they are often unsuited. +// (e.g. datetime handling of Microsoft.Data.Sqlite) + +public interface IAdminRemarksRecord +{ + public int Id { get; } + + public RoundRecord? Round { get; } + + public PlayerRecord? Player { get; } + public TimeSpan PlaytimeAtNote { get; } + + public string Message { get; } + + public PlayerRecord? CreatedBy { get; } + + public DateTimeOffset CreatedAt { get; } + + public PlayerRecord? LastEditedBy { get; } + + public DateTimeOffset? LastEditedAt { get; } + public DateTimeOffset? ExpirationTime { get; } + + public bool Deleted { get; } +} + +public sealed record ServerRoleBanNoteRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + 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, + string Message, + NoteSeverity Severity, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? UnbanningAdmin, + DateTime? UnbanTime) : IAdminRemarksRecord; + +public sealed record AdminNoteRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + NoteSeverity Severity, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt, + bool Secret) : IAdminRemarksRecord; + +public sealed record AdminWatchlistRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt) : IAdminRemarksRecord; + +public sealed record AdminMessageRecord( + int Id, + RoundRecord? Round, + PlayerRecord? Player, + TimeSpan PlaytimeAtNote, + string Message, + PlayerRecord? CreatedBy, + DateTimeOffset CreatedAt, + PlayerRecord? LastEditedBy, + DateTimeOffset? LastEditedAt, + DateTimeOffset? ExpirationTime, + bool Deleted, + PlayerRecord? DeletedBy, + DateTimeOffset? DeletedAt, + bool Seen) : IAdminRemarksRecord; + + +public sealed record PlayerRecord( + NetUserId UserId, + DateTimeOffset FirstSeenTime, + string LastSeenUserName, + DateTimeOffset LastSeenTime, + IPAddress LastSeenAddress, + ImmutableArray? HWId); + +public sealed record RoundRecord(int Id, DateTimeOffset StartDate, ServerRecord Server); + +public sealed record ServerRecord(int Id, string Name); diff --git a/Content.Server/Database/PlayerRecord.cs b/Content.Server/Database/PlayerRecord.cs deleted file mode 100644 index cfcebe1c02..0000000000 --- a/Content.Server/Database/PlayerRecord.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Immutable; -using System.Net; -using Robust.Shared.Network; - -namespace Content.Server.Database -{ - public sealed class PlayerRecord - { - public NetUserId UserId { get; } - public ImmutableArray? HWId { get; } - public DateTimeOffset FirstSeenTime { get; } - public string LastSeenUserName { get; } - public DateTimeOffset LastSeenTime { get; } - public IPAddress LastSeenAddress { get; } - - public PlayerRecord( - NetUserId userId, - DateTimeOffset firstSeenTime, - string lastSeenUserName, - DateTimeOffset lastSeenTime, - IPAddress lastSeenAddress, - ImmutableArray? hwId) - { - UserId = userId; - FirstSeenTime = firstSeenTime; - LastSeenUserName = lastSeenUserName; - LastSeenTime = lastSeenTime; - LastSeenAddress = lastSeenAddress; - HWId = hwId; - } - } -} diff --git a/Content.Server/Database/ServerBanNote.cs b/Content.Server/Database/ServerBanNote.cs deleted file mode 100644 index 4e55650090..0000000000 --- a/Content.Server/Database/ServerBanNote.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Content.Shared.Database; - -namespace Content.Server.Database -{ - public record ServerBanNote(int Id, int? RoundId, Round? Round, Guid? PlayerUserId, Player? Player, - TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, Player? CreatedBy, DateTime CreatedAt, - Player? LastEditedBy, DateTime? LastEditedAt, DateTime? ExpirationTime, bool Deleted, Player? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksCommon; -} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 27ccb6ee0e..fe42d73ae9 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -356,7 +357,7 @@ public abstract Task> GetServerBansAsync( public abstract Task AddServerBanAsync(ServerBanDef serverBan); public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); - public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -365,9 +366,9 @@ public async Task EditServerBan(int id, string reason, NoteSeverity severity, Da return; ban.Severity = severity; ban.Reason = reason; - ban.ExpirationTime = expiration; + ban.ExpirationTime = expiration?.UtcDateTime; ban.LastEditedById = editedBy; - ban.LastEditedAt = editedAt; + ban.LastEditedAt = editedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } @@ -448,7 +449,7 @@ public abstract Task> GetServerRoleBansAsync(IPAddress? a public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban); - public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -457,9 +458,9 @@ public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity return; ban.Severity = severity; ban.Reason = reason; - ban.ExpirationTime = expiration; + ban.ExpirationTime = expiration?.UtcDateTime; ban.LastEditedById = editedBy; - ban.LastEditedAt = editedAt; + ban.LastEditedAt = editedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } #endregion @@ -571,7 +572,21 @@ public async Task UpdatePlayerRecord( return record == null ? null : MakePlayerRecord(record); } - protected abstract PlayerRecord MakePlayerRecord(Player player); + [return: NotNullIfNotNull(nameof(player))] + protected PlayerRecord? MakePlayerRecord(Player? player) + { + if (player == null) + return null; + + return new PlayerRecord( + new NetUserId(player.UserId), + new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)), + player.LastSeenUserName, + new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)), + player.LastSeenAddress, + player.LastSeenHWId?.ToImmutableArray()); + } + #endregion #region Connection Logs @@ -733,6 +748,18 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} await db.DbContext.SaveChangesAsync(); } + [return: NotNullIfNotNull(nameof(round))] + protected RoundRecord? MakeRoundRecord(Round? round) + { + if (round == null) + return null; + + return new RoundRecord( + round.Id, + NormalizeDatabaseTime(round.StartDate), + MakeServerRecord(round.Server)); + } + public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) { await using var db = await GetDb(); @@ -772,6 +799,15 @@ public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) return (server, false); } + [return: NotNullIfNotNull(nameof(server))] + protected ServerRecord? MakeServerRecord(Server? server) + { + if (server == null) + return null; + + return new ServerRecord(server.Id, server.Name); + } + public async Task AddAdminLogs(List logs) { DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids."); @@ -943,17 +979,17 @@ public async Task RemoveFromWhitelistAsync(NetUserId player) await db.DbContext.SaveChangesAsync(); } - public async Task GetLastReadRules(NetUserId player) + public async Task GetLastReadRules(NetUserId player) { await using var db = await GetDb(); - return await db.DbContext.Player + return NormalizeDatabaseTime(await db.DbContext.Player .Where(dbPlayer => dbPlayer.UserId == player) .Select(dbPlayer => dbPlayer.LastReadRules) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync()); } - public async Task SetLastReadRules(NetUserId player, DateTime date) + public async Task SetLastReadRules(NetUserId player, DateTimeOffset date) { await using var db = await GetDb(); @@ -963,7 +999,7 @@ public async Task SetLastReadRules(NetUserId player, DateTime date) return; } - dbPlayer.LastReadRules = date; + dbPlayer.LastReadRules = date.UtcDateTime; await db.DbContext.SaveChangesAsync(); } @@ -971,11 +1007,11 @@ public async Task SetLastReadRules(NetUserId player, DateTime date) #region Uploaded Resources Logs - public async Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data) + public async Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data) { await using var db = await GetDb(); - db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date, Path = path, Data = data }); + db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date.UtcDateTime, Path = path, Data = data }); await db.DbContext.SaveChangesAsync(); } @@ -983,7 +1019,7 @@ public async Task PurgeUploadedResourceLogAsync(int days) { await using var db = await GetDb(); - var date = DateTime.Now.Subtract(TimeSpan.FromDays(days)); + var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(days)); await foreach (var log in db.DbContext.UploadedResourceLog .Where(l => date > l.Date) @@ -1023,10 +1059,10 @@ public virtual async Task AddAdminMessage(AdminMessage message) return message.Id; } - public async Task GetAdminNote(int id) + public async Task GetAdminNote(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminNotes + var entity = await db.DbContext.AdminNotes .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1035,12 +1071,34 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminNoteRecord(entity); } - public async Task GetAdminWatchlist(int id) + private AdminNoteRecord MakeAdminNoteRecord(AdminNote entity) + { + return new AdminNoteRecord( + entity.Id, + MakeRoundRecord(entity.Round), + MakePlayerRecord(entity.Player), + entity.PlaytimeAtNote, + entity.Message, + entity.Severity, + MakePlayerRecord(entity.CreatedBy), + NormalizeDatabaseTime(entity.CreatedAt), + MakePlayerRecord(entity.LastEditedBy), + NormalizeDatabaseTime(entity.LastEditedAt), + NormalizeDatabaseTime(entity.ExpirationTime), + entity.Deleted, + MakePlayerRecord(entity.DeletedBy), + NormalizeDatabaseTime(entity.DeletedAt), + entity.Secret); + } + + public async Task GetAdminWatchlist(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminWatchlists + var entity = await db.DbContext.AdminWatchlists .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1049,12 +1107,14 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminWatchlistRecord(entity); } - public async Task GetAdminMessage(int id) + public async Task GetAdminMessage(int id) { await using var db = await GetDb(); - return await db.DbContext.AdminMessages + var entity = await db.DbContext.AdminMessages .Where(note => note.Id == id) .Include(note => note.Round) .ThenInclude(r => r!.Server) @@ -1063,9 +1123,30 @@ public virtual async Task AddAdminMessage(AdminMessage message) .Include(note => note.DeletedBy) .Include(note => note.Player) .SingleOrDefaultAsync(); + + return entity == null ? null : MakeAdminMessageRecord(entity); } - public async Task GetServerBanAsNoteAsync(int id) + private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity) + { + return new AdminMessageRecord( + entity.Id, + MakeRoundRecord(entity.Round), + MakePlayerRecord(entity.Player), + entity.PlaytimeAtNote, + entity.Message, + MakePlayerRecord(entity.CreatedBy), + NormalizeDatabaseTime(entity.CreatedAt), + MakePlayerRecord(entity.LastEditedBy), + NormalizeDatabaseTime(entity.LastEditedAt), + NormalizeDatabaseTime(entity.ExpirationTime), + entity.Deleted, + MakePlayerRecord(entity.DeletedBy), + NormalizeDatabaseTime(entity.DeletedAt), + entity.Seen); + } + + public async Task GetServerBanAsNoteAsync(int id) { await using var db = await GetDb(); @@ -1082,22 +1163,37 @@ public virtual async Task AddAdminMessage(AdminMessage message) return null; var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - return new ServerBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, player, - ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, ban.BanTime, - ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, ban.Hidden, - ban.Unban?.UnbanningAdmin == null + 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), + p.UserId == ban.Unban.UnbanningAdmin.Value)), ban.Unban?.UnbanTime); } - public async Task GetServerRoleBanAsNoteAsync(int id) + public async Task GetServerRoleBanAsNoteAsync(int id) { await using var db = await GetDb(); var ban = await db.DbContext.RoleBan - .Include(b => b.Unban) + .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) @@ -1108,36 +1204,48 @@ public virtual async Task AddAdminMessage(AdminMessage message) ban.Unban is null ? null : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin); - return new ServerRoleBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, - player, ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, - ban.BanTime, ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, - ban.Hidden, new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, - unbanningAdmin, ban.Unban?.UnbanTime); + + return new ServerRoleBanNoteRecord( + ban.Id, + MakeRoundRecord(ban.Round), + MakePlayerRecord(player), + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy), + ban.BanTime, + MakePlayerRecord(ban.LastEditedBy), + ban.LastEditedAt, + ban.ExpirationTime, + ban.Hidden, + new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, + MakePlayerRecord(unbanningAdmin), + ban.Unban?.UnbanTime); } - public async Task> GetAllAdminRemarks(Guid player) + public async Task> GetAllAdminRemarks(Guid player) { await using var db = await GetDb(); - List notes = new(); + List notes = new(); notes.AddRange( - await (from note in db.DbContext.AdminNotes - where note.PlayerUserId == player && - !note.Deleted && - (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) - select note) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.LastEditedBy) - .Include(note => note.Player) - .ToListAsync()); + (await (from note in db.DbContext.AdminNotes + where note.PlayerUserId == player && + !note.Deleted && + (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) + select note) + .Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.LastEditedBy) + .Include(note => note.Player) + .ToListAsync()).Select(MakeAdminNoteRecord)); notes.AddRange(await GetActiveWatchlistsImpl(db, player)); notes.AddRange(await GetMessagesImpl(db, player)); notes.AddRange(await GetServerBansAsNotesForUser(db, player)); notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); return notes; } - public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); @@ -1146,39 +1254,39 @@ public async Task EditAdminNote(int id, string message, NoteSeverity severity, b note.Severity = severity; note.Secret = secret; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); var note = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync(); note.Message = message; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { await using var db = await GetDb(); var note = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync(); note.Message = message; note.LastEditedById = editedBy; - note.LastEditedAt = editedAt; - note.ExpirationTime = expiryTime; + note.LastEditedAt = editedAt.UtcDateTime; + note.ExpirationTime = expiryTime?.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1186,12 +1294,12 @@ public async Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) note.Deleted = true; note.DeletedById = deletedBy; - note.DeletedAt = deletedAt; + note.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1199,12 +1307,12 @@ public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedA watchlist.Deleted = true; watchlist.DeletedById = deletedBy; - watchlist.DeletedAt = deletedAt; + watchlist.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) + public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1212,12 +1320,12 @@ public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) message.Deleted = true; message.DeletedById = deletedBy; - message.DeletedAt = deletedAt; + message.DeletedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1225,12 +1333,12 @@ public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime delete ban.Hidden = true; ban.LastEditedById = deletedBy; - ban.LastEditedAt = deletedAt; + ban.LastEditedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -1238,40 +1346,40 @@ public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime de roleBan.Hidden = true; roleBan.LastEditedById = deletedBy; - roleBan.LastEditedAt = deletedAt; + roleBan.LastEditedAt = deletedAt.UtcDateTime; await db.DbContext.SaveChangesAsync(); } - public async Task> GetVisibleAdminRemarks(Guid player) + public async Task> GetVisibleAdminRemarks(Guid player) { await using var db = await GetDb(); - List notesCol = new(); + List notesCol = new(); notesCol.AddRange( - await (from note in db.DbContext.AdminNotes - where note.PlayerUserId == player && - !note.Secret && - !note.Deleted && - (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) - select note) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.Player) - .ToListAsync()); + (await (from note in db.DbContext.AdminNotes + where note.PlayerUserId == player && + !note.Secret && + !note.Deleted && + (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime) + select note) + .Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.Player) + .ToListAsync()).Select(MakeAdminNoteRecord)); notesCol.AddRange(await GetMessagesImpl(db, player)); return notesCol; } - public async Task> GetActiveWatchlists(Guid player) + public async Task> GetActiveWatchlists(Guid player) { await using var db = await GetDb(); return await GetActiveWatchlistsImpl(db, player); } - protected async Task> GetActiveWatchlistsImpl(DbGuard db, Guid player) + protected async Task> GetActiveWatchlistsImpl(DbGuard db, Guid player) { - return await (from watchlist in db.DbContext.AdminWatchlists + var entities = await (from watchlist in db.DbContext.AdminWatchlists where watchlist.PlayerUserId == player && !watchlist.Deleted && (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime) @@ -1282,27 +1390,34 @@ protected async Task> GetActiveWatchlistsImpl(DbGuard db, G .Include(note => note.LastEditedBy) .Include(note => note.Player) .ToListAsync(); + + return entities.Select(MakeAdminWatchlistRecord).ToList(); } - public async Task> GetMessages(Guid player) + private AdminWatchlistRecord MakeAdminWatchlistRecord(AdminWatchlist entity) + { + return new AdminWatchlistRecord(entity.Id, MakeRoundRecord(entity.Round), MakePlayerRecord(entity.Player), entity.PlaytimeAtNote, entity.Message, MakePlayerRecord(entity.CreatedBy), NormalizeDatabaseTime(entity.CreatedAt), MakePlayerRecord(entity.LastEditedBy), NormalizeDatabaseTime(entity.LastEditedAt), NormalizeDatabaseTime(entity.ExpirationTime), entity.Deleted, MakePlayerRecord(entity.DeletedBy), NormalizeDatabaseTime(entity.DeletedAt)); + } + + public async Task> GetMessages(Guid player) { await using var db = await GetDb(); return await GetMessagesImpl(db, player); } - protected async Task> GetMessagesImpl(DbGuard db, Guid player) + protected async Task> GetMessagesImpl(DbGuard db, Guid player) { - return await (from message in db.DbContext.AdminMessages - where message.PlayerUserId == player && - !message.Deleted && - (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime) - select message) - .Include(note => note.Round) - .ThenInclude(r => r!.Server) - .Include(note => note.CreatedBy) - .Include(note => note.LastEditedBy) - .Include(note => note.Player) - .ToListAsync(); + var entities = await (from message in db.DbContext.AdminMessages + where message.PlayerUserId == player && !message.Deleted && + (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime) + select message).Include(note => note.Round) + .ThenInclude(r => r!.Server) + .Include(note => note.CreatedBy) + .Include(note => note.LastEditedBy) + .Include(note => note.Player) + .ToListAsync(); + + return entities.Select(MakeAdminMessageRecord).ToList(); } public async Task MarkMessageAsSeen(int id) @@ -1314,7 +1429,7 @@ public async Task MarkMessageAsSeen(int id) } // These two are here because they get converted into notes later - protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) + protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) { // You can't group queries, as player will not always exist. When it doesn't, the // whole query returns nothing @@ -1329,17 +1444,27 @@ protected async Task> GetServerBansAsNotesForUser(DbGuard db .Include(ban => ban.Unban) .ToArrayAsync(); - var banNotes = new List(); + var banNotes = new List(); foreach (var ban in bans) { - var banNote = new ServerBanNote(ban.Id, ban.RoundId, ban.Round, ban.PlayerUserId, player, - ban.PlaytimeAtNote, ban.Reason, ban.Severity, ban.CreatedBy, ban.BanTime, - ban.LastEditedBy, ban.LastEditedAt, ban.ExpirationTime, ban.Hidden, - ban.Unban?.UnbanningAdmin == null + var banNote = new ServerBanNoteRecord( + ban.Id, + MakeRoundRecord(ban.Round), + MakePlayerRecord(player), + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy), + NormalizeDatabaseTime(ban.BanTime), + MakePlayerRecord(ban.LastEditedBy), + NormalizeDatabaseTime(ban.LastEditedAt), + NormalizeDatabaseTime(ban.ExpirationTime), + ban.Hidden, + MakePlayerRecord(ban.Unban?.UnbanningAdmin == null ? null : await db.DbContext.Player.SingleOrDefaultAsync( - p => p.UserId == ban.Unban.UnbanningAdmin.Value), - ban.Unban?.UnbanTime); + p => p.UserId == ban.Unban.UnbanningAdmin.Value)), + NormalizeDatabaseTime(ban.Unban?.UnbanTime)); banNotes.Add(banNote); } @@ -1347,7 +1472,7 @@ protected async Task> GetServerBansAsNotesForUser(DbGuard db return banNotes; } - protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) + protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) { // Server side query var bansQuery = await db.DbContext.RoleBan @@ -1366,7 +1491,7 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor .Select(banGroup => banGroup) .ToArray(); - List bans = new(); + List bans = new(); var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); foreach (var banGroup in bansEnumerable) { @@ -1376,11 +1501,22 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor if (firstBan.Unban?.UnbanningAdmin is not null) unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value); - bans.Add(new ServerRoleBanNote(firstBan.Id, firstBan.RoundId, firstBan.Round, firstBan.PlayerUserId, - player, firstBan.PlaytimeAtNote, firstBan.Reason, firstBan.Severity, firstBan.CreatedBy, - firstBan.BanTime, firstBan.LastEditedBy, firstBan.LastEditedAt, firstBan.ExpirationTime, - firstBan.Hidden, banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), - unbanningAdmin, firstBan.Unban?.UnbanTime)); + bans.Add(new ServerRoleBanNoteRecord( + firstBan.Id, + MakeRoundRecord(firstBan.Round), + MakePlayerRecord(player), + firstBan.PlaytimeAtNote, + firstBan.Reason, + firstBan.Severity, + MakePlayerRecord(firstBan.CreatedBy), + NormalizeDatabaseTime(firstBan.BanTime), + MakePlayerRecord(firstBan.LastEditedBy), + NormalizeDatabaseTime(firstBan.LastEditedAt), + NormalizeDatabaseTime(firstBan.ExpirationTime), + firstBan.Hidden, + banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), + MakePlayerRecord(unbanningAdmin), + NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); } return bans; @@ -1388,6 +1524,16 @@ protected async Task> GetGroupedServerRoleBansAsNotesFor #endregion + // SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc. + // Normalize DateTimes here so they're always Utc. Thanks. + protected abstract DateTime NormalizeDatabaseTime(DateTime time); + + [return: NotNullIfNotNull(nameof(time))] + protected DateTime? NormalizeDatabaseTime(DateTime? time) + { + return time != null ? NormalizeDatabaseTime(time.Value) : time; + } + protected abstract Task GetDb([CallerMemberName] string? name = null); protected void LogDbOp(string? name) diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 7deeeb8e95..5fda2a7e10 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -92,9 +92,9 @@ public Task EditServerBan( int id, string reason, NoteSeverity severity, - DateTime? expiration, + DateTimeOffset? expiration, Guid editedBy, - DateTime editedAt); + DateTimeOffset editedAt); /// /// Update ban exemption information for a player. @@ -146,9 +146,9 @@ public Task EditServerRoleBan( int id, string reason, NoteSeverity severity, - DateTime? expiration, + DateTimeOffset? expiration, Guid editedBy, - DateTime editedAt); + DateTimeOffset editedAt); #endregion #region Playtime @@ -239,7 +239,7 @@ Task AddConnectionLogAsync( #region Uploaded Resources Logs - Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data); + Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data); Task PurgeUploadedResourceLogAsync(int days); @@ -247,33 +247,33 @@ Task AddConnectionLogAsync( #region Rules - Task GetLastReadRules(NetUserId player); - Task SetLastReadRules(NetUserId player, DateTime time); + Task GetLastReadRules(NetUserId player); + Task SetLastReadRules(NetUserId player, DateTimeOffset time); #endregion #region Admin Notes - Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime); - Task GetAdminNote(int id); - Task GetAdminWatchlist(int id); - Task GetAdminMessage(int id); - Task GetServerBanAsNoteAsync(int id); - Task GetServerRoleBanAsNoteAsync(int id); - Task> GetAllAdminRemarks(Guid player); - Task> GetVisibleAdminNotes(Guid player); - Task> GetActiveWatchlists(Guid player); - Task> GetMessages(Guid player); - Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime); - Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt); - Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt); - Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt); - Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt); - Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt); + Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime); + Task GetAdminNote(int id); + Task GetAdminWatchlist(int id); + Task GetAdminMessage(int id); + Task GetServerBanAsNoteAsync(int id); + Task GetServerRoleBanAsNoteAsync(int id); + Task> GetAllAdminRemarks(Guid player); + Task> GetVisibleAdminNotes(Guid player); + Task> GetActiveWatchlists(Guid player); + Task> GetMessages(Guid player); + Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime); + Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); Task MarkMessageAsSeen(int id); #endregion @@ -423,7 +423,7 @@ public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); } - public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); @@ -470,7 +470,7 @@ public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban)); } - public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTime? expiration, Guid editedBy, DateTime editedAt) + 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)); @@ -665,7 +665,7 @@ public Task RemoveFromWhitelistAsync(NetUserId player) return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player)); } - public Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data) + public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.AddUploadedResourceLogAsync(user, date, path, data)); @@ -677,19 +677,19 @@ public Task PurgeUploadedResourceLogAsync(int days) return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days)); } - public Task GetLastReadRules(NetUserId player) + public Task GetLastReadRules(NetUserId player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetLastReadRules(player)); } - public Task SetLastReadRules(NetUserId player, DateTime time) + public Task SetLastReadRules(NetUserId player, DateTimeOffset time) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.SetLastReadRules(player, time)); } - public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminNote @@ -702,15 +702,15 @@ public Task AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote Message = message, Severity = severity, Secret = secret, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminNote(note)); } - public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminWatchlist @@ -721,15 +721,15 @@ public Task AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeA PlayerUserId = player, PlaytimeAtNote = playtimeAtNote, Message = message, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminWatchlist(note)); } - public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime) + public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); var note = new AdminMessage @@ -740,108 +740,108 @@ public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtN PlayerUserId = player, PlaytimeAtNote = playtimeAtNote, Message = message, - CreatedAt = createdAt, - LastEditedAt = createdAt, - ExpirationTime = expiryTime + CreatedAt = createdAt.UtcDateTime, + LastEditedAt = createdAt.UtcDateTime, + ExpirationTime = expiryTime?.UtcDateTime }; return RunDbCommand(() => _db.AddAdminMessage(note)); } - public Task GetAdminNote(int id) + public Task GetAdminNote(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminNote(id)); } - public Task GetAdminWatchlist(int id) + public Task GetAdminWatchlist(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminWatchlist(id)); } - public Task GetAdminMessage(int id) + public Task GetAdminMessage(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAdminMessage(id)); } - public Task GetServerBanAsNoteAsync(int id) + public Task GetServerBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); } - public Task GetServerRoleBanAsNoteAsync(int id) + public Task GetServerRoleBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id)); } - public Task> GetAllAdminRemarks(Guid player) + public Task> GetAllAdminRemarks(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetAllAdminRemarks(player)); } - public Task> GetVisibleAdminNotes(Guid player) + public Task> GetVisibleAdminNotes(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetVisibleAdminRemarks(player)); } - public Task> GetActiveWatchlists(Guid player) + public Task> GetActiveWatchlists(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetActiveWatchlists(player)); } - public Task> GetMessages(Guid player) + public Task> GetMessages(Guid player) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetMessages(player)); } - public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminNote(id, message, severity, secret, editedBy, editedAt, expiryTime)); } - public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, expiryTime)); } - public Task EditAdminMessage(int id, string message, Guid editedBy, DateTime editedAt, DateTime? expiryTime) + public Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.EditAdminMessage(id, message, editedBy, editedAt, expiryTime)); } - public Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminNote(id, deletedBy, deletedAt)); } - public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminWatchlist(id, deletedBy, deletedAt)); } - public Task DeleteAdminMessage(int id, Guid deletedBy, DateTime deletedAt) + public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); } - public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); } - public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTime deletedAt) + public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt)); diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index 8a8f26e503..c81e735868 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -162,7 +162,7 @@ private static IQueryable MakeBanLookupQuery( if (!includeUnbanned) { query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now)); + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } if (exemptFlags is { } exempt) @@ -354,7 +354,7 @@ private static IQueryable MakeRoleBanLookupQuery( if (!includeUnbanned) { query = query?.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now)); + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } query = query!.Distinct(); @@ -457,17 +457,6 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRole } #endregion - protected override PlayerRecord MakePlayerRecord(Player record) - { - return new PlayerRecord( - new NetUserId(record.UserId), - new DateTimeOffset(record.FirstSeenTime), - record.LastSeenUserName, - new DateTimeOffset(record.LastSeenTime), - record.LastSeenAddress, - record.LastSeenHWId?.ToImmutableArray()); - } - public override async Task AddConnectionLogAsync( NetUserId userId, string userName, @@ -532,6 +521,12 @@ WHERE to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('engl return db.AdminLog; } + protected override DateTime NormalizeDatabaseTime(DateTime time) + { + DebugTools.Assert(time.Kind == DateTimeKind.Utc); + return time; + } + private async Task GetDbImpl([CallerMemberName] string? name = null) { LogDbOp(name); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 90bbec023a..46886fe4d1 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Utility; namespace Content.Server.Database { @@ -350,17 +351,6 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba } #endregion - protected override PlayerRecord MakePlayerRecord(Player record) - { - return new PlayerRecord( - new NetUserId(record.UserId), - new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), - record.LastSeenUserName, - new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), - record.LastSeenAddress, - record.LastSeenHWId?.ToImmutableArray()); - } - private static ServerBanDef? ConvertBan(ServerBan? ban) { if (ban == null) @@ -546,6 +536,12 @@ public override async Task AddAdminMessage(AdminMessage message) return await base.AddAdminMessage(message); } + protected override DateTime NormalizeDatabaseTime(DateTime time) + { + DebugTools.Assert(time.Kind == DateTimeKind.Unspecified); + return DateTime.SpecifyKind(time, DateTimeKind.Utc); + } + private async Task GetDbImpl([CallerMemberName] string? name = null) { LogDbOp(name); diff --git a/Content.Server/Database/ServerRoleBanNote.cs b/Content.Server/Database/ServerRoleBanNote.cs deleted file mode 100644 index 6db8110db8..0000000000 --- a/Content.Server/Database/ServerRoleBanNote.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Content.Shared.Database; - -namespace Content.Server.Database -{ - public record ServerRoleBanNote(int Id, int? RoundId, Round? Round, Guid? PlayerUserId, Player? Player, - TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, Player? CreatedBy, DateTime CreatedAt, - Player? LastEditedBy, DateTime? LastEditedAt, DateTime? ExpirationTime, bool Deleted, string[] Roles, - Player? UnbanningAdmin, DateTime? UnbanTime) : IAdminRemarksCommon; -} diff --git a/Content.Shared/Administration/Notes/SharedAdminNote.cs b/Content.Shared/Administration/Notes/SharedAdminNote.cs index e209d3721e..09d4f3f947 100644 --- a/Content.Shared/Administration/Notes/SharedAdminNote.cs +++ b/Content.Shared/Administration/Notes/SharedAdminNote.cs @@ -1,4 +1,5 @@ using Content.Shared.Database; +using Robust.Shared.Network; using Robust.Shared.Serialization; namespace Content.Shared.Administration.Notes; @@ -6,7 +7,7 @@ namespace Content.Shared.Administration.Notes; [Serializable, NetSerializable] public sealed record SharedAdminNote( int Id, // Id of note, message, watchlist, ban or role ban. Should be paired with NoteType to uniquely identify a shared admin note. - Guid Player, // Notes player + NetUserId Player, // Notes player int? Round, // Which round was it added in? string? ServerName, // Which server was this added on? TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index e8f5e44a61..bc90d7942c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -794,7 +794,7 @@ public static readonly CVarDef /// Default severity for server bans /// public static readonly CVarDef ServerBanDefaultSeverity = - CVarDef.Create("admin.server_ban_default_severity", "high", CVar.ARCHIVE | CVar.SERVER); + CVarDef.Create("admin.server_ban_default_severity", "High", CVar.ARCHIVE | CVar.SERVER); /// /// Minimum explosion intensity to create an admin alert message. -1 to disable the alert.