mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-21 06:34:36 +00:00
Load database config on initialization
Adapted from RegexBot. This allows `dotnet ef` tools to make use of actual SQL credentials rather than dummy ones.
This commit is contained in:
parent
53b20db3eb
commit
b595e701c4
4 changed files with 58 additions and 69 deletions
|
@ -1,56 +1,52 @@
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
using CommandLine.Text;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Npgsql;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace WorldTime;
|
namespace WorldTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and holds configuration values.
|
/// Loads and holds configuration values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class Configuration {
|
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 BotToken { get; }
|
||||||
public string? DBotsToken { get; }
|
public string? DBotsToken { get; }
|
||||||
|
|
||||||
public int ShardTotal { get; }
|
public int ShardTotal { get; }
|
||||||
|
|
||||||
public Configuration(string[] args) {
|
public string? SqlHost { get; }
|
||||||
var cmdline = CmdLineOpts.Parse(args);
|
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
|
// Looks for configuration file
|
||||||
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + Path.DirectorySeparatorChar;
|
JObject jc;
|
||||||
confPath += cmdline.Config!;
|
try {
|
||||||
if (!File.Exists(confPath)) throw new Exception("Settings file not found in path: " + confPath);
|
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<string>(jc, nameof(BotToken), true);
|
BotToken = ReadConfKey<string>(jc, nameof(BotToken), true);
|
||||||
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
|
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
|
||||||
|
|
||||||
ShardTotal = cmdline.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1;
|
ShardTotal = args.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1;
|
||||||
if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
||||||
|
|
||||||
var sqlhost = ReadConfKey<string>(jc, KeySqlHost, false) ?? "localhost"; // Default to localhost
|
SqlHost = ReadConfKey<string>(jc, nameof(SqlHost), false);
|
||||||
var sqluser = ReadConfKey<string>(jc, KeySqlUsername, false);
|
SqlDatabase = ReadConfKey<string?>(jc, nameof(SqlDatabase), false);
|
||||||
var sqlpass = ReadConfKey<string>(jc, KeySqlPassword, false);
|
SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
|
||||||
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass))
|
SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
|
||||||
throw new Exception("'SqlUsername', 'SqlPassword' must be specified.");
|
|
||||||
var csb = new NpgsqlConnectionStringBuilder() {
|
|
||||||
Host = sqlhost,
|
|
||||||
Username = sqluser,
|
|
||||||
Password = sqlpass
|
|
||||||
};
|
|
||||||
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
|
|
||||||
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
|
||||||
DbConnectionString = csb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
||||||
|
@ -59,29 +55,24 @@ class Configuration {
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CmdLineOpts {
|
class CommandLineParameters {
|
||||||
[Option('c', "config", Default = "settings.json",
|
[Option('c', "config")]
|
||||||
HelpText = "Custom path to instance configuration, relative from executable directory.")]
|
public string? ConfigFile { get; set; }
|
||||||
public string? Config { get; set; }
|
|
||||||
|
|
||||||
[Option("shardtotal",
|
[Option("shardtotal")]
|
||||||
HelpText = "Total number of shards online. MUST be the same for all instances.\n"
|
|
||||||
+ "This value overrides the config file value.")]
|
|
||||||
public int? ShardTotal { get; set; }
|
public int? ShardTotal { get; set; }
|
||||||
|
|
||||||
public static CmdLineOpts Parse(string[] args) {
|
public static CommandLineParameters? Parse(string[] args) {
|
||||||
// Do not automatically print help message
|
CommandLineParameters? result = null;
|
||||||
var clp = new Parser(c => c.HelpWriter = null);
|
|
||||||
|
|
||||||
CmdLineOpts? result = null;
|
new Parser(settings => {
|
||||||
var r = clp.ParseArguments<CmdLineOpts>(args);
|
settings.IgnoreUnknownArguments = true;
|
||||||
r.WithParsed(parsed => result = parsed);
|
settings.AutoHelp = false;
|
||||||
r.WithNotParsed(err => {
|
settings.AutoVersion = false;
|
||||||
var ht = HelpText.AutoBuild(r);
|
}).ParseArguments<CommandLineParameters>(args)
|
||||||
Console.WriteLine(ht.ToString());
|
.WithParsed(p => result = p)
|
||||||
Environment.Exit((int)Program.ExitCodes.BadCommand);
|
.WithNotParsed(e => { /* ignore */ });
|
||||||
});
|
return result;
|
||||||
return result!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
namespace WorldTime.Data;
|
namespace WorldTime.Data;
|
||||||
public class BotDatabaseContext : DbContext {
|
public class BotDatabaseContext : DbContext {
|
||||||
private static string? _npgsqlConnectionString;
|
private static readonly string _connectionString;
|
||||||
internal static string NpgsqlConnectionString {
|
|
||||||
#if DEBUG
|
static BotDatabaseContext() {
|
||||||
get {
|
// Get our own config loaded just for the SQL stuff
|
||||||
if (_npgsqlConnectionString != null) return _npgsqlConnectionString;
|
var conf = new Configuration();
|
||||||
Program.Log(nameof(BotDatabaseContext), "Using hardcoded connection string!");
|
_connectionString = new NpgsqlConnectionStringBuilder() {
|
||||||
return _npgsqlConnectionString ?? "Host=localhost;Username=worldtime;Password=wt";
|
Host = conf.SqlHost ?? "localhost", // default to localhost
|
||||||
}
|
Database = conf.SqlDatabase,
|
||||||
#else
|
Username = conf.SqlUsername,
|
||||||
get => _npgsqlConnectionString!;
|
Password = conf.SqlPassword
|
||||||
#endif
|
}.ToString();
|
||||||
set => _npgsqlConnectionString ??= value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<UserEntry> UserEntries { get; set; } = null!;
|
public DbSet<UserEntry> UserEntries { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> optionsBuilder
|
=> optionsBuilder
|
||||||
.UseNpgsql(NpgsqlConnectionString)
|
.UseNpgsql(_connectionString)
|
||||||
#if DEBUG
|
|
||||||
.LogTo((string line) => Program.Log("EF", line), Microsoft.Extensions.Logging.LogLevel.Information)
|
|
||||||
#endif
|
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
|
|
@ -9,17 +9,15 @@ class Program {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string BotUptime => (DateTimeOffset.UtcNow - _botStartTime).ToString("d' days, 'hh':'mm':'ss");
|
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;
|
Configuration? cfg = null;
|
||||||
try {
|
try {
|
||||||
cfg = new Configuration(args);
|
cfg = new Configuration();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Console.WriteLine(ex);
|
Console.WriteLine(ex);
|
||||||
Environment.Exit((int)ExitCodes.ConfigError);
|
Environment.Exit((int)ExitCodes.ConfigError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.BotDatabaseContext.NpgsqlConnectionString = cfg.DbConnectionString;
|
|
||||||
|
|
||||||
Console.CancelKeyPress += OnCancelKeyPressed;
|
Console.CancelKeyPress += OnCancelKeyPressed;
|
||||||
_bot = new WorldTime(cfg);
|
_bot = new WorldTime(cfg);
|
||||||
await _bot.StartAsync().ConfigureAwait(false);
|
await _bot.StartAsync().ConfigureAwait(false);
|
||||||
|
|
|
@ -31,6 +31,7 @@ internal class WorldTime : IDisposable {
|
||||||
private readonly CancellationTokenSource _mainCancel;
|
private readonly CancellationTokenSource _mainCancel;
|
||||||
private readonly CommandsText _commandsTxt;
|
private readonly CommandsText _commandsTxt;
|
||||||
private readonly IServiceProvider _services;
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly HashSet<ulong> _aotUserDownloadChecked = new();
|
||||||
|
|
||||||
internal Configuration Config { get; }
|
internal Configuration Config { get; }
|
||||||
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
||||||
|
@ -186,7 +187,9 @@ internal class WorldTime : IDisposable {
|
||||||
if (message.Channel is not SocketTextChannel channel) return;
|
if (message.Channel is not SocketTextChannel channel) return;
|
||||||
|
|
||||||
// Proactively fill guild user cache if the bot has any data for the respective guild
|
// 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) {
|
if (!channel.Guild.HasAllMembers) {
|
||||||
using var db = _services.GetRequiredService<BotDatabaseContext>();
|
using var db = _services.GetRequiredService<BotDatabaseContext>();
|
||||||
if (db.HasAnyUsers(channel.Guild)) {
|
if (db.HasAnyUsers(channel.Guild)) {
|
||||||
|
|
Loading…
Reference in a new issue