using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using Content.Shared; using Content.Shared.Preferences; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Npgsql; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Network; using LogLevel = Robust.Shared.Log.LogLevel; using MSLogLevel = Microsoft.Extensions.Logging.LogLevel; #nullable enable namespace Content.Server.Database { public interface IServerDbManager { void Init(); // Preferences Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile); 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 GetPlayerPreferencesAsync(NetUserId userId); // Username assignment (for guest accounts, so they persist GUID) Task AssignUserIdAsync(string name, NetUserId userId); Task GetAssignedUserIdAsync(string name); // Ban stuff /// /// Looks up a ban by id. /// This will return a pardoned ban as well. /// /// The ban id to look for. /// The ban with the given id or null if none exist. Task GetServerBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. /// This will NOT return a pardoned ban. /// One of or need to not be null. /// /// The ip address of the user. /// The id of the user. /// The hardware ID of the user. /// The user's latest received un-pardoned ban, or null if none exist. Task GetServerBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId); /// /// Looks up an user's ban history. /// This will return pardoned bans as well. /// One of or need to not be null. /// /// The ip address of the user. /// The id of the user. /// The HWId of the user. /// The user's ban history. Task> GetServerBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId); Task AddServerBanAsync(ServerBanDef serverBan); Task AddServerUnbanAsync(ServerUnbanDef serverBan); // Player records Task UpdatePlayerRecordAsync( NetUserId userId, string userName, IPAddress address, ImmutableArray hwId); Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default); Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default); // Connection log Task AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, ImmutableArray hwId); // Admins Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default); Task 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); Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default); Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default); Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default); } public sealed class ServerDbManager : IServerDbManager { [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!; public void Init() { _msLogProvider = new LoggingProvider(_logMgr); _msLoggerFactory = LoggerFactory.Create(builder => { builder.AddProvider(_msLogProvider); }); var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower(); switch (engine) { case "sqlite": var options = CreateSqliteOptions(); _db = new ServerDbSqlite(options); break; case "postgres": options = CreatePostgresOptions(); _db = new ServerDbPostgres(options); break; default: throw new InvalidDataException($"Unknown database engine {engine}."); } } public Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile) { return _db.InitPrefsAsync(userId, defaultProfile); } public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) { return _db.SaveSelectedCharacterIndexAsync(userId, index); } public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot) { return _db.SaveCharacterSlotAsync(userId, profile, slot); } public Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot) { return _db.DeleteSlotAndSetSelectedIndex(userId, deleteSlot, newSlot); } public Task SaveAdminOOCColorAsync(NetUserId userId, Color color) { return _db.SaveAdminOOCColorAsync(userId, color); } public Task GetPlayerPreferencesAsync(NetUserId userId) { return _db.GetPlayerPreferencesAsync(userId); } public Task AssignUserIdAsync(string name, NetUserId userId) { return _db.AssignUserIdAsync(name, userId); } public Task GetAssignedUserIdAsync(string name) { return _db.GetAssignedUserIdAsync(name); } public Task GetServerBanAsync(int id) { return _db.GetServerBanAsync(id); } public Task GetServerBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId) { return _db.GetServerBanAsync(address, userId, hwId); } public Task> GetServerBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId) { return _db.GetServerBansAsync(address, userId, hwId); } public Task AddServerBanAsync(ServerBanDef serverBan) { return _db.AddServerBanAsync(serverBan); } public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) { return _db.AddServerUnbanAsync(serverUnban); } public Task UpdatePlayerRecordAsync( NetUserId userId, string userName, IPAddress address, ImmutableArray hwId) { return _db.UpdatePlayerRecord(userId, userName, address, hwId); } public Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default) { return _db.GetPlayerRecordByUserName(userName, cancel); } public Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default) { return _db.GetPlayerRecordByUserId(userId, cancel); } public Task AddConnectionLogAsync( NetUserId userId, string userName, IPAddress address, ImmutableArray hwId) { return _db.AddConnectionLogAsync(userId, userName, address, hwId); } public Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default) { return _db.GetAdminDataForAsync(userId, cancel); } public Task GetAdminRankAsync(int id, CancellationToken cancel = default) { return _db.GetAdminRankDataForAsync(id, cancel); } public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( CancellationToken cancel = default) { return _db.GetAllAdminAndRanksAsync(cancel); } public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default) { return _db.RemoveAdminAsync(userId, cancel); } public Task AddAdminAsync(Admin admin, CancellationToken cancel = default) { return _db.AddAdminAsync(admin, cancel); } public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default) { return _db.UpdateAdminAsync(admin, cancel); } public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default) { return _db.RemoveAdminRankAsync(rankId, cancel); } public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default) { return _db.AddAdminRankAsync(rank, cancel); } public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default) { return _db.UpdateAdminRankAsync(rank, cancel); } private DbContextOptions 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(); var connectionString = new NpgsqlConnectionStringBuilder { Host = host, Port = port, Database = db, Username = user, Password = pass }.ConnectionString; builder.UseNpgsql(connectionString); SetupLogging(builder); return builder.Options; } private DbContextOptions CreateSqliteOptions() { var builder = new DbContextOptionsBuilder(); var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath); var inMemory = _res.UserData.RootDir == null; SqliteConnection connection; if (!inMemory) { var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath); connection = new SqliteConnection($"Data Source={finalPreferencesDbPath}"); } else { connection = 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. connection.Open(); } builder.UseSqlite(connection); 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(MSLogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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 state) { // TODO: this return null!; } } } }