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 @@ 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; } 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 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) 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 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; } 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 @@ 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; } 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 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 ); } 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 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, 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 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> 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 @@ 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 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 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 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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> 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 @@ 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> 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 @@ 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> 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 @@ 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> 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} .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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} 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 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id} .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 @@ 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 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 @@ namespace Content.Server.Database 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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 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 @@ 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 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 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 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 @@ namespace Content.Shared.CCVar /// 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.