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)) {