From b595e701c43924ecfeea1f94aa90feedfd67fb3d Mon Sep 17 00:00:00 2001 From: Noi Date: Tue, 9 Aug 2022 18:09:08 -0700 Subject: [PATCH] Load database config on initialization Adapted from RegexBot. This allows `dotnet ef` tools to make use of actual SQL credentials rather than dummy ones. --- Configuration.cs | 87 +++++++++++++++++--------------------- Data/BotDatabaseContext.cs | 29 ++++++------- Program.cs | 6 +-- WorldTime.cs | 5 ++- 4 files changed, 58 insertions(+), 69 deletions(-) diff --git a/Configuration.cs b/Configuration.cs index 571c1be..2568669 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -1,56 +1,52 @@ using CommandLine; -using CommandLine.Text; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Npgsql; using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace WorldTime; - /// /// Loads and holds configuration values. /// class Configuration { - const string KeySqlHost = "SqlHost"; - const string KeySqlUsername = "SqlUsername"; - const string KeySqlPassword = "SqlPassword"; - const string KeySqlDatabase = "SqlDatabase"; - - public string DbConnectionString { get; } public string BotToken { get; } public string? DBotsToken { get; } public int ShardTotal { get; } - public Configuration(string[] args) { - var cmdline = CmdLineOpts.Parse(args); + public string? SqlHost { get; } + public string? SqlDatabase { get; } + public string SqlUsername { get; } + public string SqlPassword { get; } + + public Configuration() { + var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs()); + var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + + Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "settings.json"; // Looks for configuration file - var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + Path.DirectorySeparatorChar; - confPath += cmdline.Config!; - if (!File.Exists(confPath)) throw new Exception("Settings file not found in path: " + confPath); + JObject jc; + try { + var conftxt = File.ReadAllText(path); + jc = JObject.Parse(conftxt); + } catch (Exception ex) { + string pfx; + if (ex is JsonException) pfx = "Unable to parse configuration: "; + else pfx = "Unable to access configuration: "; - var jc = JObject.Parse(File.ReadAllText(confPath)); + throw new Exception(pfx + ex.Message, ex); + } BotToken = ReadConfKey(jc, nameof(BotToken), true); DBotsToken = ReadConfKey(jc, nameof(DBotsToken), false); - ShardTotal = cmdline.ShardTotal ?? ReadConfKey(jc, nameof(ShardTotal), false) ?? 1; + ShardTotal = args.ShardTotal ?? ReadConfKey(jc, nameof(ShardTotal), false) ?? 1; if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer."); - var sqlhost = ReadConfKey(jc, KeySqlHost, false) ?? "localhost"; // Default to localhost - var sqluser = ReadConfKey(jc, KeySqlUsername, false); - var sqlpass = ReadConfKey(jc, KeySqlPassword, false); - if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass)) - throw new Exception("'SqlUsername', 'SqlPassword' must be specified."); - var csb = new NpgsqlConnectionStringBuilder() { - Host = sqlhost, - Username = sqluser, - Password = sqlpass - }; - var sqldb = ReadConfKey(jc, KeySqlDatabase, false); - if (sqldb != null) csb.Database = sqldb; // Optional database setting - DbConnectionString = csb.ToString(); + SqlHost = ReadConfKey(jc, nameof(SqlHost), false); + SqlDatabase = ReadConfKey(jc, nameof(SqlDatabase), false); + SqlUsername = ReadConfKey(jc, nameof(SqlUsername), true); + SqlPassword = ReadConfKey(jc, nameof(SqlPassword), true); } private static T? ReadConfKey(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) { @@ -59,29 +55,24 @@ class Configuration { return default; } - private class CmdLineOpts { - [Option('c', "config", Default = "settings.json", - HelpText = "Custom path to instance configuration, relative from executable directory.")] - public string? Config { get; set; } + class CommandLineParameters { + [Option('c', "config")] + public string? ConfigFile { get; set; } - [Option("shardtotal", - HelpText = "Total number of shards online. MUST be the same for all instances.\n" - + "This value overrides the config file value.")] + [Option("shardtotal")] public int? ShardTotal { get; set; } - public static CmdLineOpts Parse(string[] args) { - // Do not automatically print help message - var clp = new Parser(c => c.HelpWriter = null); + public static CommandLineParameters? Parse(string[] args) { + CommandLineParameters? result = null; - CmdLineOpts? result = null; - var r = clp.ParseArguments(args); - r.WithParsed(parsed => result = parsed); - r.WithNotParsed(err => { - var ht = HelpText.AutoBuild(r); - Console.WriteLine(ht.ToString()); - Environment.Exit((int)Program.ExitCodes.BadCommand); - }); - return result!; + new Parser(settings => { + settings.IgnoreUnknownArguments = true; + settings.AutoHelp = false; + settings.AutoVersion = false; + }).ParseArguments(args) + .WithParsed(p => result = p) + .WithNotParsed(e => { /* ignore */ }); + return result; } } } diff --git a/Data/BotDatabaseContext.cs b/Data/BotDatabaseContext.cs index 03ac6d1..52c4298 100644 --- a/Data/BotDatabaseContext.cs +++ b/Data/BotDatabaseContext.cs @@ -1,29 +1,26 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; namespace WorldTime.Data; public class BotDatabaseContext : DbContext { - private static string? _npgsqlConnectionString; - internal static string NpgsqlConnectionString { -#if DEBUG - get { - if (_npgsqlConnectionString != null) return _npgsqlConnectionString; - Program.Log(nameof(BotDatabaseContext), "Using hardcoded connection string!"); - return _npgsqlConnectionString ?? "Host=localhost;Username=worldtime;Password=wt"; - } -#else - get => _npgsqlConnectionString!; -#endif - set => _npgsqlConnectionString ??= value; + private static readonly string _connectionString; + + static BotDatabaseContext() { + // Get our own config loaded just for the SQL stuff + var conf = new Configuration(); + _connectionString = new NpgsqlConnectionStringBuilder() { + Host = conf.SqlHost ?? "localhost", // default to localhost + Database = conf.SqlDatabase, + Username = conf.SqlUsername, + Password = conf.SqlPassword + }.ToString(); } public DbSet UserEntries { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(NpgsqlConnectionString) -#if DEBUG - .LogTo((string line) => Program.Log("EF", line), Microsoft.Extensions.Logging.LogLevel.Information) -#endif + .UseNpgsql(_connectionString) .UseSnakeCaseNamingConvention(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Program.cs b/Program.cs index d61ffe2..3562520 100644 --- a/Program.cs +++ b/Program.cs @@ -9,17 +9,15 @@ class Program { /// public static string BotUptime => (DateTimeOffset.UtcNow - _botStartTime).ToString("d' days, 'hh':'mm':'ss"); - static async Task Main(string[] args) { + static async Task Main() { Configuration? cfg = null; try { - cfg = new Configuration(args); + cfg = new Configuration(); } catch (Exception ex) { Console.WriteLine(ex); Environment.Exit((int)ExitCodes.ConfigError); } - Data.BotDatabaseContext.NpgsqlConnectionString = cfg.DbConnectionString; - Console.CancelKeyPress += OnCancelKeyPressed; _bot = new WorldTime(cfg); await _bot.StartAsync().ConfigureAwait(false); diff --git a/WorldTime.cs b/WorldTime.cs index d4648ff..fb8444b 100644 --- a/WorldTime.cs +++ b/WorldTime.cs @@ -31,6 +31,7 @@ internal class WorldTime : IDisposable { private readonly CancellationTokenSource _mainCancel; private readonly CommandsText _commandsTxt; private readonly IServiceProvider _services; + private readonly HashSet _aotUserDownloadChecked = new(); internal Configuration Config { get; } internal DiscordShardedClient DiscordClient => _services.GetRequiredService(); @@ -186,7 +187,9 @@ internal class WorldTime : IDisposable { if (message.Channel is not SocketTextChannel channel) return; // Proactively fill guild user cache if the bot has any data for the respective guild - // Can skip an extra query if the last_seen update is known to have been successful, otherwise query for any users + lock (_aotUserDownloadChecked) { + if (!_aotUserDownloadChecked.Add(channel.Guild.Id)) return; // ...once. Just once. Not all the time. + } if (!channel.Guild.HasAllMembers) { using var db = _services.GetRequiredService(); if (db.HasAnyUsers(channel.Guild)) {