| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303 |
- using System.Collections.Immutable;
- using System.IO;
- using System.Net;
- using System.Text.Json;
- using System.Threading;
- using System.Threading.Tasks;
- using Content.Server.Administration.Logs;
- using Content.Shared.Administration.Logs;
- using Content.Shared.CCVar;
- using Content.Shared.Database;
- using Content.Shared.Preferences;
- using Content.Shared.Roles;
- using Microsoft.Data.Sqlite;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Logging;
- using Npgsql;
- using Prometheus;
- using Robust.Shared.Configuration;
- using Robust.Shared.ContentPack;
- using Robust.Shared.Network;
- using Robust.Shared.Prototypes;
- using LogLevel = Robust.Shared.Log.LogLevel;
- using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
- namespace Content.Server.Database
- {
- public interface IServerDbManager
- {
- void Init();
- void Shutdown();
- #region Preferences
- Task<PlayerPreferences> InitPrefsAsync(
- NetUserId userId,
- ICharacterProfile defaultProfile,
- CancellationToken cancel);
- Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
- Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
- Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
- // Single method for two operations for transaction.
- Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
- Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
- #endregion
- #region User Ids
- // Username assignment (for guest accounts, so they persist GUID)
- Task AssignUserIdAsync(string name, NetUserId userId);
- Task<NetUserId?> GetAssignedUserIdAsync(string name);
- #endregion
- #region Bans
- /// <summary>
- /// Looks up a ban by id.
- /// This will return a pardoned ban as well.
- /// </summary>
- /// <param name="id">The ban id to look for.</param>
- /// <returns>The ban with the given id or null if none exist.</returns>
- Task<ServerBanDef?> GetServerBanAsync(int id);
- /// <summary>
- /// Looks up an user's most recent received un-pardoned ban.
- /// This will NOT return a pardoned ban.
- /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
- /// </summary>
- /// <param name="address">The ip address of the user.</param>
- /// <param name="userId">The id of the user.</param>
- /// <param name="hwId">The legacy HWID of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
- Task<ServerBanDef?> GetServerBanAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds);
- /// <summary>
- /// Looks up an user's ban history.
- /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
- /// </summary>
- /// <param name="address">The ip address of the user.</param>
- /// <param name="userId">The id of the user.</param>
- /// <param name="hwId">The legacy HWId of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
- /// <returns>The user's ban history.</returns>
- Task<List<ServerBanDef>> GetServerBansAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned=true);
- Task AddServerBanAsync(ServerBanDef serverBan);
- Task AddServerUnbanAsync(ServerUnbanDef serverBan);
- public Task EditServerBan(
- int id,
- string reason,
- NoteSeverity severity,
- DateTimeOffset? expiration,
- Guid editedBy,
- DateTimeOffset editedAt);
- /// <summary>
- /// Update ban exemption information for a player.
- /// </summary>
- /// <remarks>
- /// Database rows are automatically created and removed when appropriate.
- /// </remarks>
- /// <param name="userId">The user to update</param>
- /// <param name="flags">The new ban exemption flags.</param>
- Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags);
- /// <summary>
- /// Get current ban exemption flags for a user
- /// </summary>
- /// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
- Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default);
- #endregion
- #region Role Bans
- /// <summary>
- /// Looks up a role ban by id.
- /// This will return a pardoned role ban as well.
- /// </summary>
- /// <param name="id">The role ban id to look for.</param>
- /// <returns>The role ban with the given id or null if none exist.</returns>
- Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
- /// <summary>
- /// Looks up an user's role ban history.
- /// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
- /// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
- /// </summary>
- /// <param name="address">The IP address of the user.</param>
- /// <param name="userId">The NetUserId of the user.</param>
- /// <param name="hwId">The Hardware Id of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
- /// <returns>The user's role ban history.</returns>
- Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned = true);
- Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
- Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
- public Task EditServerRoleBan(
- int id,
- string reason,
- NoteSeverity severity,
- DateTimeOffset? expiration,
- Guid editedBy,
- DateTimeOffset editedAt);
- #endregion
- #region Playtime
- /// <summary>
- /// Look up a player's role timers.
- /// </summary>
- /// <param name="player">The player to get the role timer information from.</param>
- /// <param name="cancel"></param>
- /// <returns>All role timers belonging to the player.</returns>
- Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel = default);
- /// <summary>
- /// Update play time information in bulk.
- /// </summary>
- /// <param name="updates">The list of all updates to apply to the database.</param>
- Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates);
- #endregion
- #region Player Records
- Task UpdatePlayerRecordAsync(
- NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId);
- Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
- Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
- #endregion
- #region Connection Logs
- /// <returns>ID of newly inserted connection log row.</returns>
- Task<int> AddConnectionLogAsync(
- NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId,
- float trust,
- ConnectionDenyReason? denied,
- int serverId);
- Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans);
- #endregion
- #region Admin Ranks
- Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default);
- Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default);
- Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
- CancellationToken cancel = default);
- Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default);
- Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
- Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
- /// <summary>
- /// Update whether an admin has voluntarily deadminned.
- /// </summary>
- /// <remarks>
- /// This does nothing if the player is not an admin.
- /// </remarks>
- /// <param name="userId">The user ID of the admin.</param>
- /// <param name="deadminned">Whether the admin is deadminned or not.</param>
- Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default);
- Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
- Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
- Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
- #endregion
- #region Rounds
- Task<int> AddNewRound(Server server, params Guid[] playerIds);
- Task<Round> GetRound(int id);
- Task AddRoundPlayers(int id, params Guid[] playerIds);
- #endregion
- #region Admin Logs
- Task<Server> AddOrGetServer(string serverName);
- Task AddAdminLogs(List<AdminLog> logs);
- IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null);
- IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null);
- IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null);
- Task<int> CountAdminLogs(int round);
- #endregion
- #region Whitelist
- Task<bool> GetWhitelistStatusAsync(NetUserId player);
- Task AddToWhitelistAsync(NetUserId player);
- Task RemoveFromWhitelistAsync(NetUserId player);
- #endregion
- #region Blacklist
- Task<bool> GetBlacklistStatusAsync(NetUserId player);
- Task AddToBlacklistAsync(NetUserId player);
- Task RemoveFromBlacklistAsync(NetUserId player);
- #endregion
- #region Uploaded Resources Logs
- Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
- Task PurgeUploadedResourceLogAsync(int days);
- #endregion
- #region Rules
- 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, 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);
- /// <summary>
- /// Mark an admin message as being seen by the target player.
- /// </summary>
- /// <param name="id">The database ID of the admin message.</param>
- /// <param name="dismissedToo">
- /// If true, the message is "permanently dismissed" and will not be shown to the player again when they join.
- /// </param>
- Task MarkMessageAsSeen(int id, bool dismissedToo);
- #endregion
- #region Job Whitelists
- Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job);
- Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default);
- Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job);
- Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
- #endregion
- #region IPintel
- Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score);
- Task<IPIntelCache?> GetIPIntelCache(IPAddress ip);
- Task<bool> CleanIPIntelCache(TimeSpan range);
- #endregion
- #region DB Notifications
- void SubscribeToNotifications(Action<DatabaseNotification> handler);
- /// <summary>
- /// Inject a notification as if it was created by the database. This is intended for testing.
- /// </summary>
- /// <param name="notification">The notification to trigger</param>
- void InjectTestNotification(DatabaseNotification notification);
- /// <summary>
- /// Send a notification to all other servers connected to the same database.
- /// </summary>
- /// <remarks>
- /// The local server will receive the sent notification itself again.
- /// </remarks>
- /// <param name="notification">The notification to send.</param>
- Task SendNotification(DatabaseNotification notification);
- #endregion
- }
- /// <summary>
- /// Represents a notification sent between servers via the database layer.
- /// </summary>
- /// <remarks>
- /// <para>
- /// Database notifications are a simple system to broadcast messages to an entire server group
- /// backed by the same database. For example, this is used to notify all servers of new ban records.
- /// </para>
- /// <para>
- /// They are currently implemented by the PostgreSQL <c>NOTIFY</c> and <c>LISTEN</c> commands.
- /// </para>
- /// </remarks>
- public struct DatabaseNotification
- {
- /// <summary>
- /// The channel for the notification. This can be used to differentiate notifications for different purposes.
- /// </summary>
- public required string Channel { get; set; }
- /// <summary>
- /// The actual contents of the notification. Optional.
- /// </summary>
- public string? Payload { get; set; }
- }
- public sealed class ServerDbManager : IServerDbManager
- {
- public static readonly Counter DbReadOpsMetric = Metrics.CreateCounter(
- "db_read_ops",
- "Amount of read operations processed by the database manager.");
- public static readonly Counter DbWriteOpsMetric = Metrics.CreateCounter(
- "db_write_ops",
- "Amount of write operations processed by the database manager.");
- public static readonly Gauge DbActiveOps = Metrics.CreateGauge(
- "db_executing_ops",
- "Amount of active database operations. Note that some operations may be waiting for a database connection.");
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IResourceManager _res = default!;
- [Dependency] private readonly ILogManager _logMgr = default!;
- private ServerDbBase _db = default!;
- private LoggingProvider _msLogProvider = default!;
- private ILoggerFactory _msLoggerFactory = default!;
- private bool _synchronous;
- // When running in integration tests, we'll use a single in-memory SQLite database connection.
- // This is that connection, close it when we shut down.
- private SqliteConnection? _sqliteInMemoryConnection;
- private readonly List<Action<DatabaseNotification>> _notificationHandlers = [];
- public void Init()
- {
- _msLogProvider = new LoggingProvider(_logMgr);
- _msLoggerFactory = LoggerFactory.Create(builder =>
- {
- builder.AddProvider(_msLogProvider);
- });
- _synchronous = _cfg.GetCVar(CCVars.DatabaseSynchronous);
- var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
- var opsLog = _logMgr.GetSawmill("db.op");
- var notifyLog = _logMgr.GetSawmill("db.notify");
- switch (engine)
- {
- case "sqlite":
- SetupSqlite(out var contextFunc, out var inMemory);
- _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
- break;
- case "postgres":
- var (pgOptions, conString) = CreatePostgresOptions();
- _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
- break;
- default:
- throw new InvalidDataException($"Unknown database engine {engine}.");
- }
- _db.OnNotificationReceived += HandleDatabaseNotification;
- }
- public void Shutdown()
- {
- _db.OnNotificationReceived -= HandleDatabaseNotification;
- _sqliteInMemoryConnection?.Dispose();
- _db.Shutdown();
- }
- public Task<PlayerPreferences> InitPrefsAsync(
- NetUserId userId,
- ICharacterProfile defaultProfile,
- CancellationToken cancel)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.InitPrefsAsync(userId, defaultProfile));
- }
- public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.SaveSelectedCharacterIndexAsync(userId, index));
- }
- public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.SaveCharacterSlotAsync(userId, profile, slot));
- }
- public Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.DeleteSlotAndSetSelectedIndex(userId, deleteSlot, newSlot));
- }
- public Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
- }
- public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel));
- }
- public Task AssignUserIdAsync(string name, NetUserId userId)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AssignUserIdAsync(name, userId));
- }
- public Task<NetUserId?> GetAssignedUserIdAsync(string name)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAssignedUserIdAsync(name));
- }
- public Task<ServerBanDef?> GetServerBanAsync(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerBanAsync(id));
- }
- public Task<ServerBanDef?> GetServerBanAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
- }
- public Task<List<ServerBanDef>> GetServerBansAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned=true)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
- }
- public Task AddServerBanAsync(ServerBanDef serverBan)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddServerBanAsync(serverBan));
- }
- public Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban));
- }
- 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));
- }
- public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
- }
- public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
- }
- #region Role Ban
- public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerRoleBanAsync(id));
- }
- public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned = true)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
- }
- public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan));
- }
- public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban));
- }
- public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt));
- }
- #endregion
- #region Playtime
- public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetPlayTimes(player, cancel));
- }
- public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdatePlayTimes(updates));
- }
- #endregion
- public Task UpdatePlayerRecordAsync(
- NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
- }
- public Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetPlayerRecordByUserName(userName, cancel));
- }
- public Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetPlayerRecordByUserId(userId, cancel));
- }
- public Task<int> AddConnectionLogAsync(
- NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId,
- float trust,
- ConnectionDenyReason? denied,
- int serverId)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
- }
- public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans));
- }
- public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminDataForAsync(userId, cancel));
- }
- public Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminRankDataForAsync(id, cancel));
- }
- public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
- CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAllAdminAndRanksAsync(cancel));
- }
- public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.RemoveAdminAsync(userId, cancel));
- }
- public Task AddAdminAsync(Admin admin, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddAdminAsync(admin, cancel));
- }
- public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
- }
- public Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdateAdminDeadminnedAsync(userId, deadminned, cancel));
- }
- public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.RemoveAdminRankAsync(rankId, cancel));
- }
- public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddAdminRankAsync(rank, cancel));
- }
- public Task<int> AddNewRound(Server server, params Guid[] playerIds)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddNewRound(server, playerIds));
- }
- public Task<Round> GetRound(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetRound(id));
- }
- public Task AddRoundPlayers(int id, params Guid[] playerIds)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddRoundPlayers(id, playerIds));
- }
- public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpdateAdminRankAsync(rank, cancel));
- }
- public async Task<Server> AddOrGetServer(string serverName)
- {
- var (server, existed) = await RunDbCommand(() => _db.AddOrGetServer(serverName));
- if (existed)
- DbReadOpsMetric.Inc();
- else
- DbWriteOpsMetric.Inc();
- return server;
- }
- public Task AddAdminLogs(List<AdminLog> logs)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddAdminLogs(logs));
- }
- public IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminLogMessages(filter));
- }
- public IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminLogs(filter));
- }
- public IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminLogsJson(filter));
- }
- public Task<int> CountAdminLogs(int round)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.CountAdminLogs(round));
- }
- public Task<bool> GetWhitelistStatusAsync(NetUserId player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetWhitelistStatusAsync(player));
- }
- public Task AddToWhitelistAsync(NetUserId player)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddToWhitelistAsync(player));
- }
- public Task RemoveFromWhitelistAsync(NetUserId player)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player));
- }
- public Task<bool> GetBlacklistStatusAsync(NetUserId player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetBlacklistStatusAsync(player));
- }
- public Task AddToBlacklistAsync(NetUserId player)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddToBlacklistAsync(player));
- }
- public Task RemoveFromBlacklistAsync(NetUserId player)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.RemoveFromBlacklistAsync(player));
- }
- public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddUploadedResourceLogAsync(user, date, path, data));
- }
- public Task PurgeUploadedResourceLogAsync(int days)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days));
- }
- public Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetLastReadRules(player));
- }
- 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, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
- {
- DbWriteOpsMetric.Inc();
- var note = new AdminNote
- {
- RoundId = roundId,
- CreatedById = createdBy,
- LastEditedById = createdBy,
- PlayerUserId = player,
- PlaytimeAtNote = playtimeAtNote,
- Message = message,
- Severity = severity,
- Secret = secret,
- 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, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
- {
- DbWriteOpsMetric.Inc();
- var note = new AdminWatchlist
- {
- RoundId = roundId,
- CreatedById = createdBy,
- LastEditedById = createdBy,
- PlayerUserId = player,
- PlaytimeAtNote = playtimeAtNote,
- Message = message,
- 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, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
- {
- DbWriteOpsMetric.Inc();
- var note = new AdminMessage
- {
- RoundId = roundId,
- CreatedById = createdBy,
- LastEditedById = createdBy,
- PlayerUserId = player,
- PlaytimeAtNote = playtimeAtNote,
- Message = message,
- CreatedAt = createdAt.UtcDateTime,
- LastEditedAt = createdAt.UtcDateTime,
- ExpirationTime = expiryTime?.UtcDateTime
- };
- return RunDbCommand(() => _db.AddAdminMessage(note));
- }
- public Task<AdminNoteRecord?> GetAdminNote(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminNote(id));
- }
- public Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminWatchlist(id));
- }
- public Task<AdminMessageRecord?> GetAdminMessage(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAdminMessage(id));
- }
- public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id));
- }
- public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
- }
- public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetAllAdminRemarks(player));
- }
- public Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetVisibleAdminRemarks(player));
- }
- public Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetActiveWatchlists(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, 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, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, 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, DateTimeOffset deletedAt)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.DeleteAdminNote(id, deletedBy, 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, DateTimeOffset deletedAt)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt));
- }
- public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt));
- }
- public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt));
- }
- public Task MarkMessageAsSeen(int id, bool dismissedToo)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
- }
- public Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.AddJobWhitelist(player, job));
- }
- public Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetJobWhitelists(player, cancel));
- }
- public Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
- {
- DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.IsJobWhitelisted(player, job));
- }
- public Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
- }
- public Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.UpsertIPIntelCache(time, ip, score));
- }
- public Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
- {
- return RunDbCommand(() => _db.GetIPIntelCache(ip));
- }
- public Task<bool> CleanIPIntelCache(TimeSpan range)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.CleanIPIntelCache(range));
- }
- public void SubscribeToNotifications(Action<DatabaseNotification> handler)
- {
- lock (_notificationHandlers)
- {
- _notificationHandlers.Add(handler);
- }
- }
- public void InjectTestNotification(DatabaseNotification notification)
- {
- HandleDatabaseNotification(notification);
- }
- public Task SendNotification(DatabaseNotification notification)
- {
- DbWriteOpsMetric.Inc();
- return RunDbCommand(() => _db.SendNotification(notification));
- }
- private async void HandleDatabaseNotification(DatabaseNotification notification)
- {
- lock (_notificationHandlers)
- {
- foreach (var handler in _notificationHandlers)
- {
- handler(notification);
- }
- }
- }
- // Wrapper functions to run DB commands from the thread pool.
- // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
- // For SQLite, this will also enable read parallelization (within limits).
- //
- // If we're configured to be synchronous (for integration tests) we shouldn't thread pool it,
- // as that would make things very random and undeterministic.
- // That only works on SQLite though, since SQLite is internally synchronous anyways.
- private async Task<T> RunDbCommand<T>(Func<Task<T>> command)
- {
- using var _ = DbActiveOps.TrackInProgress();
- if (_synchronous)
- return await RunDbCommandCoreSync(command);
- return await Task.Run(command);
- }
- private async Task RunDbCommand(Func<Task> command)
- {
- using var _ = DbActiveOps.TrackInProgress();
- if (_synchronous)
- {
- await RunDbCommandCoreSync(command);
- return;
- }
- await Task.Run(command);
- }
- private static T RunDbCommandCoreSync<T>(Func<T> command) where T : IAsyncResult
- {
- var task = command();
- if (!task.IsCompleted)
- {
- // We can't just do BlockWaitOnTask here, because that could cause deadlocks.
- // This flag is only intended for integration tests. If we trip this, it's a bug.
- throw new InvalidOperationException(
- "Database task is running asynchronously. " +
- "This should be impossible when the database is set to synchronous.");
- }
- return task;
- }
- private IAsyncEnumerable<T> RunDbCommand<T>(Func<IAsyncEnumerable<T>> command)
- {
- var enumerable = command();
- if (_synchronous)
- return new SyncAsyncEnumerable<T>(enumerable);
- return enumerable;
- }
- private (DbContextOptions<PostgresServerDbContext> options, string connectionString) CreatePostgresOptions()
- {
- var host = _cfg.GetCVar(CCVars.DatabasePgHost);
- var port = _cfg.GetCVar(CCVars.DatabasePgPort);
- var db = _cfg.GetCVar(CCVars.DatabasePgDatabase);
- var user = _cfg.GetCVar(CCVars.DatabasePgUsername);
- var pass = _cfg.GetCVar(CCVars.DatabasePgPassword);
- var builder = new DbContextOptionsBuilder<PostgresServerDbContext>();
- var connectionString = new NpgsqlConnectionStringBuilder
- {
- Host = host,
- Port = port,
- Database = db,
- Username = user,
- Password = pass
- }.ConnectionString;
- Logger.DebugS("db.manager", $"Using Postgres \"{host}:{port}/{db}\"");
- builder.UseNpgsql(connectionString);
- SetupLogging(builder);
- return (builder.Options, connectionString);
- }
- private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory)
- {
- #if USE_SYSTEM_SQLITE
- SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
- #endif
- // Can't re-use the SqliteConnection across multiple threads, so we have to make it every time.
- Func<SqliteConnection> getConnection;
- var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath);
- inMemory = _res.UserData.RootDir == null;
- if (!inMemory)
- {
- var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath);
- Logger.DebugS("db.manager", $"Using SQLite DB \"{finalPreferencesDbPath}\"");
- getConnection = () => new SqliteConnection($"Data Source={finalPreferencesDbPath}");
- }
- else
- {
- Logger.DebugS("db.manager", "Using in-memory SQLite DB");
- _sqliteInMemoryConnection = new SqliteConnection("Data Source=:memory:");
- // When using an in-memory DB we have to open it manually
- // so EFCore doesn't open, close and wipe it every operation.
- _sqliteInMemoryConnection.Open();
- getConnection = () => _sqliteInMemoryConnection;
- }
- contextFunc = () =>
- {
- var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
- builder.UseSqlite(getConnection());
- SetupLogging(builder);
- return builder.Options;
- };
- }
- private void SetupLogging(DbContextOptionsBuilder builder)
- {
- builder.UseLoggerFactory(_msLoggerFactory);
- }
- private sealed class LoggingProvider : ILoggerProvider
- {
- private readonly ILogManager _logManager;
- public LoggingProvider(ILogManager logManager)
- {
- _logManager = logManager;
- }
- public void Dispose()
- {
- }
- public ILogger CreateLogger(string categoryName)
- {
- return new MSLogger(_logManager.GetSawmill("db.ef"));
- }
- }
- private sealed class MSLogger : ILogger
- {
- private readonly ISawmill _sawmill;
- public MSLogger(ISawmill sawmill)
- {
- _sawmill = sawmill;
- }
- public void Log<TState>(MSLogLevel logLevel, EventId eventId, TState state, Exception? exception,
- Func<TState, Exception?, string> formatter)
- {
- var lvl = logLevel switch
- {
- MSLogLevel.Trace => LogLevel.Debug,
- MSLogLevel.Debug => LogLevel.Debug,
- // EFCore feels the need to log individual DB commands as "Information" so I'm slapping debug on it.
- MSLogLevel.Information => LogLevel.Debug,
- MSLogLevel.Warning => LogLevel.Warning,
- MSLogLevel.Error => LogLevel.Error,
- MSLogLevel.Critical => LogLevel.Fatal,
- MSLogLevel.None => LogLevel.Debug,
- _ => LogLevel.Debug
- };
- _sawmill.Log(lvl, formatter(state, exception));
- }
- public bool IsEnabled(MSLogLevel logLevel)
- {
- return true;
- }
- public IDisposable? BeginScope<TState>(TState state) where TState : notnull
- {
- // TODO: this
- return null;
- }
- }
- }
- public sealed record PlayTimeUpdate(NetUserId User, string Tracker, TimeSpan Time);
- internal sealed class SyncAsyncEnumerable<T> : IAsyncEnumerable<T>
- {
- private readonly IAsyncEnumerable<T> _enumerable;
- public SyncAsyncEnumerable(IAsyncEnumerable<T> enumerable)
- {
- _enumerable = enumerable;
- }
- public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
- {
- return new Enumerator(_enumerable.GetAsyncEnumerator(cancellationToken));
- }
- private sealed class Enumerator : IAsyncEnumerator<T>
- {
- private readonly IAsyncEnumerator<T> _enumerator;
- public Enumerator(IAsyncEnumerator<T> enumerator)
- {
- _enumerator = enumerator;
- }
- public ValueTask DisposeAsync()
- {
- var task = _enumerator.DisposeAsync();
- if (!task.IsCompleted)
- throw new InvalidOperationException("DisposeAsync did not complete synchronously.");
- return task;
- }
- public ValueTask<bool> MoveNextAsync()
- {
- var task = _enumerator.MoveNextAsync();
- if (!task.IsCompleted)
- throw new InvalidOperationException("MoveNextAsync did not complete synchronously.");
- return task;
- }
- public T Current => _enumerator.Current;
- }
- }
- }
|