]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Fix admin notes and database time nonsense. (#25280)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Tue, 20 Feb 2024 09:13:31 +0000 (10:13 +0100)
committerGitHub <noreply@github.com>
Tue, 20 Feb 2024 09:13:31 +0000 (10:13 +0100)
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

19 files changed:
Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs
Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
Content.Server.Database/Model.cs
Content.Server.Database/ModelPostgres.cs
Content.Server/Administration/Notes/AdminMessageEui.cs
Content.Server/Administration/Notes/AdminNotesExtensions.cs
Content.Server/Administration/Notes/AdminNotesManager.cs
Content.Server/Administration/Notes/IAdminNotesManager.cs
Content.Server/Database/DatabaseRecords.cs [new file with mode: 0644]
Content.Server/Database/PlayerRecord.cs [deleted file]
Content.Server/Database/ServerBanNote.cs [deleted file]
Content.Server/Database/ServerDbBase.cs
Content.Server/Database/ServerDbManager.cs
Content.Server/Database/ServerDbPostgres.cs
Content.Server/Database/ServerDbSqlite.cs
Content.Server/Database/ServerRoleBanNote.cs [deleted file]
Content.Shared/Administration/Notes/SharedAdminNote.cs
Content.Shared/CCVar/CCVars.cs

index e70248880b03bd23519175289cd43ed27fe7f999..ead1d8b00e5240725c2c41c304237e779f6aed34 100644 (file)
@@ -68,7 +68,7 @@ public sealed partial class AdminNotesLine : BoxContainer
             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 @@ public sealed partial class AdminNotesLine : BoxContainer
             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 @@ public sealed partial class AdminNotesLine : BoxContainer
 
         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;
         }
 
index 5ef29513e24a55ca957135b81b26cd284a117bd6..18a50031582d722cc8dd85f3c9510bd37bc85315 100644 (file)
@@ -36,12 +36,12 @@ public sealed partial class AdminNotesLinePopup : Popup
             ? 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)
index 77dde4688d2b422bc3ccbe8c2d6008d14ad930de..6f314f79542e12eab7da5c0c5d54278a2e987bf8 100644 (file)
@@ -81,7 +81,7 @@ public sealed partial class NoteEdit : FancyWindow
             {
                 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 @@ public sealed partial class NoteEdit : FancyWindow
         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 @@ public sealed partial class NoteEdit : FancyWindow
             return false;
         }
 
-        ExpiryTime = result;
+        ExpiryTime = result.ToUniversalTime();
         ExpiryLineEdit.ModulateSelfOverride = null;
         return true;
     }
index d6dec1dc3e1f4ccc2271991ee0b2b78eb10aa335..f9ec7811f5d9687d654ba81914631d947c08b556 100644 (file)
@@ -874,33 +874,8 @@ namespace Content.Server.Database
         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 @@ namespace Content.Server.Database
     }
 
     [Index(nameof(PlayerUserId))]
-    public class AdminWatchlist : IAdminRemarksCommon
+    public class AdminWatchlist
     {
         [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
 
@@ -965,7 +940,7 @@ namespace Content.Server.Database
     }
 
     [Index(nameof(PlayerUserId))]
-    public class AdminMessage : IAdminRemarksCommon
+    public class AdminMessage
     {
         [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
 
index a6b1856ab17661070024eede33d27bb3770f3573..7499d0b0f5956974e795b48651a10e8e2c27ccf1 100644 (file)
@@ -10,11 +10,6 @@ namespace Content.Server.Database
 {
     public sealed class PostgresServerDbContext : ServerDbContext
     {
-        static PostgresServerDbContext()
-        {
-            AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
-        }
-
         public PostgresServerDbContext(DbContextOptions<PostgresServerDbContext> options) : base(options)
         {
         }
index ddb91aca7c637a906732c1867c9df09332ed7129..c5e0b60172c99ebb297d4b38493605ed21fdf052 100644 (file)
@@ -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 sealed class AdminMessageEui : BaseEui
         _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 sealed class AdminMessageEui : BaseEui
             _closeWait,
             _message.Message,
             _message.CreatedBy?.LastSeenUserName ?? "[System]",
-            _message.CreatedAt
+            _message.CreatedAt.UtcDateTime
         );
     }
 
index 44ad20eec6736beced85ebaf1213413ae263d495..349c7ff3bdfbd54915e2328055bd9510dd88ef0c 100644 (file)
@@ -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 class AdminNotesExtensions
         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 class AdminNotesExtensions
         }
 
         // 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 class AdminNotesExtensions
             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,
index 0c1e7f3daade17b296cfc3a4b5db4b48e4e26669..e09e1906486ee69d8ddfce50513ff1a993a4db72 100644 (file)
@@ -144,7 +144,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
 
         var note = new SharedAdminNote(
             noteId,
-            player,
+            (NetUserId) player,
             roundId,
             serverName,
             playtime,
@@ -306,27 +306,27 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
         NoteModified?.Invoke(newNote);
     }
 
-    public async Task<List<IAdminRemarksCommon>> GetAllAdminRemarks(Guid player)
+    public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
     {
         return await _db.GetAllAdminRemarks(player);
     }
 
-    public async Task<List<IAdminRemarksCommon>> GetVisibleRemarks(Guid player)
+    public async Task<List<IAdminRemarksRecord>> 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<IAdminRemarksCommon>();
+        return new List<IAdminRemarksRecord>();
     }
 
-    public async Task<List<AdminWatchlist>> GetActiveWatchlists(Guid player)
+    public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
     {
         return await _db.GetActiveWatchlists(player);
     }
 
-    public async Task<List<AdminMessage>> GetNewMessages(Guid player)
+    public async Task<List<AdminMessageRecord>> GetNewMessages(Guid player)
     {
         return await _db.GetMessages(player);
     }
index a726bd11c82b76e82e1585e56d3aac4b4bbabee6..81ebd3e7166183aa427e5afe8ce14c6318a62d3c 100644 (file)
@@ -26,24 +26,24 @@ public interface IAdminNotesManager
     /// </summary>
     /// <param name="player">Desired player's <see cref="Guid"/></param>
     /// <returns>ALL non-deleted notes, secret or not</returns>
-    Task<List<IAdminRemarksCommon>> GetAllAdminRemarks(Guid player);
+    Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
     /// <summary>
     /// Queries the database and retrieves the notes a player should see
     /// </summary>
     /// <param name="player">Desired player's <see cref="Guid"/></param>
     /// <returns>All player-visible notes</returns>
-    Task<List<IAdminRemarksCommon>> GetVisibleRemarks(Guid player);
+    Task<List<IAdminRemarksRecord>> GetVisibleRemarks(Guid player);
     /// <summary>
     /// Queries the database and retrieves watchlists that may have been placed on the player
     /// </summary>
     /// <param name="player">Desired player's <see cref="Guid"/></param>
     /// <returns>Active watchlists</returns>
-    Task<List<AdminWatchlist>> GetActiveWatchlists(Guid player);
+    Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player);
     /// <summary>
     /// Queries the database and retrieves new messages a player has gotten
     /// </summary>
     /// <param name="player">Desired player's <see cref="Guid"/></param>
     /// <returns>All unread messages</returns>
-    Task<List<AdminMessage>> GetNewMessages(Guid player);
+    Task<List<AdminMessageRecord>> 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 (file)
index 0000000..af740a4
--- /dev/null
@@ -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<byte>? 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 (file)
index cfcebe1..0000000
+++ /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<byte>? 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<byte>? 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 (file)
index 4e55650..0000000
+++ /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;
-}
index 27ccb6ee0ea70ba28943533ba6c14c01f07547db..fe42d73ae95016f9cd07863e1d73163050edad97 100644 (file)
@@ -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 @@ namespace Content.Server.Database
         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 @@ namespace Content.Server.Database
                 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 @@ namespace Content.Server.Database
         public abstract Task<ServerRoleBanDef> 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 @@ namespace Content.Server.Database
                 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 @@ namespace Content.Server.Database
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
             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<AdminLog> logs)
         {
             DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
@@ -943,17 +979,17 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
             await db.DbContext.SaveChangesAsync();
         }
 
-        public async Task<DateTime?> GetLastReadRules(NetUserId player)
+        public async Task<DateTimeOffset?> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 return;
             }
 
-            dbPlayer.LastReadRules = date;
+            dbPlayer.LastReadRules = date.UtcDateTime;
             await db.DbContext.SaveChangesAsync();
         }
 
@@ -971,11 +1007,11 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
         #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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
         {
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
             return message.Id;
         }
 
-        public async Task<AdminNote?> GetAdminNote(int id)
+        public async Task<AdminNoteRecord?> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 .Include(note => note.DeletedBy)
                 .Include(note => note.Player)
                 .SingleOrDefaultAsync();
+
+            return entity == null ? null : MakeAdminNoteRecord(entity);
         }
 
-        public async Task<AdminWatchlist?> 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<AdminWatchlistRecord?> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 .Include(note => note.DeletedBy)
                 .Include(note => note.Player)
                 .SingleOrDefaultAsync();
+
+            return entity == null ? null : MakeAdminWatchlistRecord(entity);
         }
 
-        public async Task<AdminMessage?> GetAdminMessage(int id)
+        public async Task<AdminMessageRecord?> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 .Include(note => note.DeletedBy)
                 .Include(note => note.Player)
                 .SingleOrDefaultAsync();
+
+            return entity == null ? null : MakeAdminMessageRecord(entity);
         }
 
-        public async Task<ServerBanNote?> 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<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
         {
             await using var db = await GetDb();
 
@@ -1082,22 +1163,37 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 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<ServerRoleBanNote?> GetServerRoleBanAsNoteAsync(int id)
+        public async Task<ServerRoleBanNoteRecord?> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 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<List<IAdminRemarksCommon>> GetAllAdminRemarks(Guid player)
+        public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
         {
             await using var db = await GetDb();
-            List<IAdminRemarksCommon> notes = new();
+            List<IAdminRemarksRecord> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
             roleBan.Hidden = true;
             roleBan.LastEditedById = deletedBy;
-            roleBan.LastEditedAt = deletedAt;
+            roleBan.LastEditedAt = deletedAt.UtcDateTime;
 
             await db.DbContext.SaveChangesAsync();
         }
 
-        public async Task<List<IAdminRemarksCommon>> GetVisibleAdminRemarks(Guid player)
+        public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player)
         {
             await using var db = await GetDb();
-            List<IAdminRemarksCommon> notesCol = new();
+            List<IAdminRemarksRecord> 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<List<AdminWatchlist>> GetActiveWatchlists(Guid player)
+        public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
         {
             await using var db = await GetDb();
             return await GetActiveWatchlistsImpl(db, player);
         }
 
-        protected async Task<List<AdminWatchlist>> GetActiveWatchlistsImpl(DbGuard db, Guid player)
+        protected async Task<List<AdminWatchlistRecord>> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 .Include(note => note.LastEditedBy)
                 .Include(note => note.Player)
                 .ToListAsync();
+
+            return entities.Select(MakeAdminWatchlistRecord).ToList();
         }
 
-        public async Task<List<AdminMessage>> 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<List<AdminMessageRecord>> GetMessages(Guid player)
         {
             await using var db = await GetDb();
             return await GetMessagesImpl(db, player);
         }
 
-        protected async Task<List<AdminMessage>> GetMessagesImpl(DbGuard db, Guid player)
+        protected async Task<List<AdminMessageRecord>> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
         }
 
         // These two are here because they get converted into notes later
-        protected async Task<List<ServerBanNote>> GetServerBansAsNotesForUser(DbGuard db, Guid user)
+        protected async Task<List<ServerBanNoteRecord>> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 .Include(ban => ban.Unban)
                 .ToArrayAsync();
 
-            var banNotes = new List<ServerBanNote>();
+            var banNotes = new List<ServerBanNoteRecord>();
             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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
             return banNotes;
         }
 
-        protected async Task<List<ServerRoleBanNote>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
+        protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
         {
             // Server side query
             var bansQuery = await db.DbContext.RoleBan
@@ -1366,7 +1491,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                     .Select(banGroup => banGroup)
                     .ToArray();
 
-            List<ServerRoleBanNote> bans = new();
+            List<ServerRoleBanNoteRecord> bans = new();
             var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
             foreach (var banGroup in bansEnumerable)
             {
@@ -1376,11 +1501,22 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
 
         #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<DbGuard> GetDb([CallerMemberName] string? name = null);
 
         protected void LogDbOp(string? name)
index 7deeeb8e95acb2771067e9666b9a7a39d3eefa40..5fda2a7e10b111d81020cb39b9cc43499dc9c281 100644 (file)
@@ -92,9 +92,9 @@ namespace Content.Server.Database
             int id,
             string reason,
             NoteSeverity severity,
-            DateTime? expiration,
+            DateTimeOffset? expiration,
             Guid editedBy,
-            DateTime editedAt);
+            DateTimeOffset editedAt);
 
         /// <summary>
         /// Update ban exemption information for a player.
@@ -146,9 +146,9 @@ namespace Content.Server.Database
             int id,
             string reason,
             NoteSeverity severity,
-            DateTime? expiration,
+            DateTimeOffset? expiration,
             Guid editedBy,
-            DateTime editedAt);
+            DateTimeOffset editedAt);
         #endregion
 
         #region Playtime
@@ -239,7 +239,7 @@ namespace Content.Server.Database
 
         #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 @@ namespace Content.Server.Database
 
         #region Rules
 
-        Task<DateTime?> GetLastReadRules(NetUserId player);
-        Task SetLastReadRules(NetUserId player, DateTime time);
+        Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
+        Task SetLastReadRules(NetUserId player, DateTimeOffset time);
 
         #endregion
 
         #region Admin Notes
 
-        Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime);
-        Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime);
-        Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime);
-        Task<AdminNote?> GetAdminNote(int id);
-        Task<AdminWatchlist?> GetAdminWatchlist(int id);
-        Task<AdminMessage?> GetAdminMessage(int id);
-        Task<ServerBanNote?> GetServerBanAsNoteAsync(int id);
-        Task<ServerRoleBanNote?> GetServerRoleBanAsNoteAsync(int id);
-        Task<List<IAdminRemarksCommon>> GetAllAdminRemarks(Guid player);
-        Task<List<IAdminRemarksCommon>> GetVisibleAdminNotes(Guid player);
-        Task<List<AdminWatchlist>> GetActiveWatchlists(Guid player);
-        Task<List<AdminMessage>> 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<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
+        Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
+        Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
+        Task<AdminNoteRecord?> GetAdminNote(int id);
+        Task<AdminWatchlistRecord?> GetAdminWatchlist(int id);
+        Task<AdminMessageRecord?> GetAdminMessage(int id);
+        Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id);
+        Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id);
+        Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
+        Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player);
+        Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player);
+        Task<List<AdminMessageRecord>> 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 @@ namespace Content.Server.Database
             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 @@ namespace Content.Server.Database
             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 @@ namespace Content.Server.Database
             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 @@ namespace Content.Server.Database
             return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days));
         }
 
-        public Task<DateTime?> GetLastReadRules(NetUserId player)
+        public Task<DateTimeOffset?> 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<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTime createdAt, DateTime? expiryTime)
+        public Task<int> 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 @@ namespace Content.Server.Database
                 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<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime)
+        public Task<int> 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 @@ namespace Content.Server.Database
                 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<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTime createdAt, DateTime? expiryTime)
+        public Task<int> 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 @@ namespace Content.Server.Database
                 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<AdminNote?> GetAdminNote(int id)
+        public Task<AdminNoteRecord?> GetAdminNote(int id)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetAdminNote(id));
         }
-        public Task<AdminWatchlist?> GetAdminWatchlist(int id)
+        public Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetAdminWatchlist(id));
         }
-        public Task<AdminMessage?> GetAdminMessage(int id)
+        public Task<AdminMessageRecord?> GetAdminMessage(int id)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetAdminMessage(id));
         }
 
-        public Task<ServerBanNote?> GetServerBanAsNoteAsync(int id)
+        public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id));
         }
 
-        public Task<ServerRoleBanNote?> GetServerRoleBanAsNoteAsync(int id)
+        public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
         }
 
-    public Task<List<IAdminRemarksCommon>> GetAllAdminRemarks(Guid player)
+    public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetAllAdminRemarks(player));
         }
 
-        public Task<List<IAdminRemarksCommon>> GetVisibleAdminNotes(Guid player)
+        public Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetVisibleAdminRemarks(player));
         }
 
-        public Task<List<AdminWatchlist>> GetActiveWatchlists(Guid player)
+        public Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
         {
             DbReadOpsMetric.Inc();
             return RunDbCommand(() => _db.GetActiveWatchlists(player));
         }
 
-        public Task<List<AdminMessage>> GetMessages(Guid player)
+        public Task<List<AdminMessageRecord>> 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));
index 8a8f26e503b1fe613826ff82ac551cec540df60a..c81e735868ad26c23064d6ce5495d05ddb607a9f 100644 (file)
@@ -162,7 +162,7 @@ namespace Content.Server.Database
             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 @@ namespace Content.Server.Database
             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 @@ namespace Content.Server.Database
         }
         #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<int> 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<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
         {
             LogDbOp(name);
index 90bbec023a8830c4b03892619abdcd1c70b2151d..46886fe4d1822404e02c6f03456dbf14aff8d76a 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
 using Microsoft.EntityFrameworkCore;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
+using Robust.Shared.Utility;
 
 namespace Content.Server.Database
 {
@@ -350,17 +351,6 @@ namespace Content.Server.Database
         }
         #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 @@ namespace Content.Server.Database
             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<DbGuardImpl> 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 (file)
index 6db8110..0000000
+++ /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;
-}
index e209d3721e9edd3dd0da2d7e7027743b16a4bae4..09d4f3f9478fbc1e62c515681cfca90c04a5892c 100644 (file)
@@ -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
index e8f5e44a616bfebc7072046a767131c3ecaf182d..bc90d7942c2c8a83c4a7e629637617e83028dabd 100644 (file)
@@ -794,7 +794,7 @@ namespace Content.Shared.CCVar
         /// Default severity for server bans
         /// </summary>
         public static readonly CVarDef<string> 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);
 
         /// <summary>
         ///     Minimum explosion intensity to create an admin alert message. -1 to disable the alert.