mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 01:14:12 +00:00
commit
619c7a8f7f
13 changed files with 190 additions and 134 deletions
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||||
|
"name": ".NET Core Launch (console)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/bin/Debug/net6.0/BirthdayBot.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/BirthdayBot.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/BirthdayBot.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/BirthdayBot.csproj"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
abstract class BackgroundService {
|
abstract class BackgroundService {
|
||||||
|
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
|
||||||
protected ShardInstance ShardInstance { get; }
|
protected ShardInstance ShardInstance { get; }
|
||||||
|
|
||||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||||
|
|
|
@ -3,7 +3,6 @@ using NodaTime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
||||||
/// and optionally sends the announcement message to appropriate guilds.
|
/// and optionally sends the announcement message to appropriate guilds.
|
||||||
|
@ -15,11 +14,22 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// Processes birthday updates for all available guilds synchronously.
|
/// Processes birthday updates for all available guilds synchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
|
try {
|
||||||
|
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||||
|
await ProcessBirthdaysAsync(token);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
DbConcurrentOperationsLock.Release();
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessBirthdaysAsync(CancellationToken token) {
|
||||||
// For database efficiency, fetch all database information at once before proceeding
|
// For database efficiency, fetch all database information at once before proceeding
|
||||||
using var db = new BotDatabaseContext();
|
using var db = new BotDatabaseContext();
|
||||||
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
|
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
|
||||||
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
||||||
var guildChecks = presentGuildSettings.ToList().Select(s => new Tuple<ulong, GuildConfig>((ulong)s.GuildId, s));
|
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create((ulong)s.GuildId, s));
|
||||||
|
|
||||||
var exceptions = new List<Exception>();
|
var exceptions = new List<Exception>();
|
||||||
foreach (var (guildId, settings) in guildChecks) {
|
foreach (var (guildId, settings) in guildChecks) {
|
||||||
|
@ -44,7 +54,7 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
// Invalid role was configured. Clear the setting and quit.
|
// Invalid role was configured. Clear the setting and quit.
|
||||||
settings.RoleId = null;
|
settings.RoleId = null;
|
||||||
db.Update(settings);
|
db.Update(settings);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,13 +112,13 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// Gets all known users from the given guild and returns a list including only those who are
|
/// Gets all known users from the given guild and returns a list including only those who are
|
||||||
/// currently experiencing a birthday in the respective time zone.
|
/// currently experiencing a birthday in the respective time zone.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<UserEntry> guildUsers, string? ServerDefaultTzId) {
|
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<UserEntry> guildUsers, string? serverDefaultTzId) {
|
||||||
var birthdayUsers = new HashSet<ulong>();
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
|
|
||||||
foreach (var record in guildUsers) {
|
foreach (var record in guildUsers) {
|
||||||
// Determine final time zone to use for calculation
|
// Determine final time zone to use for calculation
|
||||||
DateTimeZone tz = DateTimeZoneProviders.Tzdb
|
DateTimeZone tz = DateTimeZoneProviders.Tzdb
|
||||||
.GetZoneOrNull(record.TimeZone ?? ServerDefaultTzId ?? "UTC")!;
|
.GetZoneOrNull(record.TimeZone ?? serverDefaultTzId ?? "UTC")!;
|
||||||
|
|
||||||
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||||
// Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar
|
// Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar
|
||||||
|
|
|
@ -2,13 +2,12 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class DataRetention : BackgroundService {
|
class DataRetention : BackgroundService {
|
||||||
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
|
||||||
const int ProcessInterval = 5400 / ShardBackgroundWorker.Interval; // Process about once per hour and a half
|
const int ProcessInterval = 5400 / ShardBackgroundWorker.Interval; // Process about once per hour and a half
|
||||||
|
|
||||||
// Amount of days without updates before data is considered stale and up for deletion.
|
// Amount of days without updates before data is considered stale and up for deletion.
|
||||||
const int StaleGuildThreshold = 180;
|
const int StaleGuildThreshold = 180;
|
||||||
const int StaleUserThreashold = 360;
|
const int StaleUserThreashold = 360;
|
||||||
|
@ -19,6 +18,17 @@ class DataRetention : BackgroundService {
|
||||||
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
||||||
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||||
|
await RemoveStaleEntriesAsync();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
DbConcurrentOperationsLock.Release();
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveStaleEntriesAsync() {
|
||||||
using var db = new BotDatabaseContext();
|
using var db = new BotDatabaseContext();
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
int updatedGuilds = 0, updatedUsers = 0;
|
int updatedGuilds = 0, updatedUsers = 0;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the execution of periodic background tasks specific to each shard.
|
/// Handles the execution of periodic background tasks specific to each shard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -54,7 +53,7 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// Skip this round of task execution if the client is not connected
|
// Skip this round of task execution if the client is not connected
|
||||||
if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue;
|
if (Instance.DiscordClient.ConnectionState != ConnectionState.Connected) continue;
|
||||||
|
|
||||||
// Execute tasks sequentially
|
// Execute tasks sequentially
|
||||||
foreach (var service in _workers) {
|
foreach (var service in _workers) {
|
||||||
|
@ -62,8 +61,9 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
try {
|
try {
|
||||||
if (_workerCanceller.IsCancellationRequested) break;
|
if (_workerCanceller.IsCancellationRequested) break;
|
||||||
_tickCount++;
|
_tickCount++;
|
||||||
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
await service.OnTick(_tickCount, _workerCanceller.Token);
|
||||||
} catch (Exception ex) when (ex is not TaskCanceledException) {
|
} catch (Exception ex) when (ex is not
|
||||||
|
(TaskCanceledException or OperationCanceledException or ObjectDisposedException)) {
|
||||||
Instance.Log(CurrentExecutingService, ex.ToString());
|
Instance.Log(CurrentExecutingService, ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>3.4.2</Version>
|
<Version>3.4.3</Version>
|
||||||
<Authors>NoiTheCat</Authors>
|
<Authors>NoiTheCat</Authors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -22,17 +22,17 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.7.0" />
|
<PackageReference Include="Discord.Net" Version="3.7.2" />
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="NodaTime" Version="3.1.0" />
|
<PackageReference Include="NodaTime" Version="3.1.0" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.4" />
|
<PackageReference Include="Npgsql" Version="6.0.6" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 16
|
|
||||||
VisualStudioVersion = 16.0.29806.167
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirthdayBot", "BirthdayBot.csproj", "{804EFB38-1D30-4082-B1F7-CD2594E9490E}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{804EFB38-1D30-4082-B1F7-CD2594E9490E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{804EFB38-1D30-4082-B1F7-CD2594E9490E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{804EFB38-1D30-4082-B1F7-CD2594E9490E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{804EFB38-1D30-4082-B1F7-CD2594E9490E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {2929174F-3E37-41FB-83CF-9EF38AFD225C}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
|
@ -1,8 +1,6 @@
|
||||||
using BirthdayBot.Data;
|
using CommandLine;
|
||||||
using CommandLine;
|
using Newtonsoft.Json;
|
||||||
using CommandLine.Text;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Npgsql;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -13,10 +11,6 @@ namespace BirthdayBot;
|
||||||
/// 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";
|
|
||||||
const string KeyShardRange = "ShardRange";
|
const string KeyShardRange = "ShardRange";
|
||||||
|
|
||||||
public string BotToken { get; }
|
public string BotToken { get; }
|
||||||
|
@ -27,32 +21,44 @@ class Configuration {
|
||||||
public int ShardAmount { get; }
|
public int ShardAmount { get; }
|
||||||
public int ShardTotal { get; }
|
public int ShardTotal { get; }
|
||||||
|
|
||||||
public string DatabaseConnectionString { get; }
|
public string? SqlHost { get; }
|
||||||
|
public string? SqlDatabase { get; }
|
||||||
|
public string SqlUsername { get; }
|
||||||
|
public string SqlPassword { get; }
|
||||||
|
internal string SqlApplicationName { get; }
|
||||||
|
|
||||||
public Configuration(string[] args) {
|
public Configuration() {
|
||||||
var cmdline = CmdLineOpts.Parse(args);
|
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);
|
||||||
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false;
|
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? 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.");
|
||||||
|
|
||||||
string shardRangeInput = cmdline.ShardRange ?? ReadConfKey<string>(jc, KeyShardRange, false);
|
var shardRangeInput = args.ShardRange ?? ReadConfKey<string>(jc, KeyShardRange, false);
|
||||||
if (!string.IsNullOrWhiteSpace(shardRangeInput)) {
|
if (!string.IsNullOrWhiteSpace(shardRangeInput)) {
|
||||||
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
||||||
var m = srPicker.Match(shardRangeInput);
|
var m = srPicker.Match(shardRangeInput);
|
||||||
if (m.Success) {
|
if (m.Success) {
|
||||||
ShardStart = int.Parse(m.Groups["low"].Value);
|
ShardStart = int.Parse(m.Groups["low"].Value);
|
||||||
int high = int.Parse(m.Groups["high"].Value);
|
var high = int.Parse(m.Groups["high"].Value);
|
||||||
ShardAmount = high - (ShardStart - 1);
|
ShardAmount = high - (ShardStart - 1);
|
||||||
} else {
|
} else {
|
||||||
throw new Exception($"Shard range not properly formatted in '{KeyShardRange}'.");
|
throw new Exception($"Shard range not properly formatted in '{KeyShardRange}'.");
|
||||||
|
@ -63,20 +69,11 @@ class Configuration {
|
||||||
ShardAmount = ShardTotal;
|
ShardAmount = ShardTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.");
|
SqlApplicationName = $"ClientShard{ShardStart}+{ShardAmount}";
|
||||||
var csb = new NpgsqlConnectionStringBuilder() {
|
|
||||||
Host = sqlhost,
|
|
||||||
Username = sqluser,
|
|
||||||
Password = sqlpass,
|
|
||||||
ApplicationName = $"ClientShard{ShardStart}+{ShardAmount}"
|
|
||||||
};
|
|
||||||
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
|
|
||||||
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
|
||||||
DatabaseConnectionString = 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) {
|
||||||
|
@ -85,33 +82,27 @@ 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; }
|
||||||
|
|
||||||
[Option("shardrange", HelpText = "Shard range for this instance to handle.\n"
|
[Option("shardrange")]
|
||||||
+ "This value overrides the config file value.")]
|
|
||||||
public string? ShardRange { get; set; }
|
public string? ShardRange { 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,20 +1,22 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
namespace BirthdayBot.Data;
|
namespace BirthdayBot.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=birthdaybot;Password=bb";
|
Host = conf.SqlHost ?? "localhost", // default to localhost
|
||||||
}
|
Database = conf.SqlDatabase,
|
||||||
#else
|
Username = conf.SqlUsername,
|
||||||
get => _npgsqlConnectionString!;
|
Password = conf.SqlPassword,
|
||||||
#endif
|
ApplicationName = conf.SqlApplicationName,
|
||||||
set => _npgsqlConnectionString ??= value;
|
MaxPoolSize = Math.Max((int)Math.Ceiling(conf.ShardAmount * 2 * 0.6), 8)
|
||||||
|
}.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<BlocklistEntry> BlocklistEntries { get; set; } = null!;
|
public DbSet<BlocklistEntry> BlocklistEntries { get; set; } = null!;
|
||||||
|
@ -22,11 +24,8 @@ public class BotDatabaseContext : DbContext {
|
||||||
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) {
|
||||||
|
|
23
Program.cs
23
Program.cs
|
@ -14,21 +14,20 @@ class Program {
|
||||||
static async Task Main(string[] args) {
|
static async Task Main(string[] args) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
BotDatabaseContext.NpgsqlConnectionString = cfg.DatabaseConnectionString;
|
Database.DBConnectionString = new Npgsql.NpgsqlConnectionStringBuilder() {
|
||||||
|
Host = cfg.SqlHost ?? "localhost", // default to localhost
|
||||||
Database.DBConnectionString = cfg.DatabaseConnectionString;
|
Database = cfg.SqlDatabase,
|
||||||
try {
|
Username = cfg.SqlUsername,
|
||||||
await Database.DoInitialDatabaseSetupAsync();
|
Password = cfg.SqlPassword,
|
||||||
} catch (Npgsql.NpgsqlException e) {
|
ApplicationName = cfg.SqlApplicationName,
|
||||||
Console.WriteLine("Error when attempting to connect to database: " + e.Message);
|
MaxPoolSize = Math.Max((int)Math.Ceiling(cfg.ShardAmount * 2 * 0.6), 8)
|
||||||
Environment.Exit((int)ExitCodes.DatabaseError);
|
}.ToString();
|
||||||
}
|
|
||||||
|
|
||||||
Console.CancelKeyPress += OnCancelKeyPressed;
|
Console.CancelKeyPress += OnCancelKeyPressed;
|
||||||
_bot = new ShardManager(cfg);
|
_bot = new ShardManager(cfg);
|
||||||
|
@ -40,10 +39,10 @@ class Program {
|
||||||
/// Sends a formatted message to console.
|
/// Sends a formatted message to console.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Log(string source, string message) {
|
public static void Log(string source, string message) {
|
||||||
var ts = DateTime.UtcNow;
|
var ts = DateTime.Now;
|
||||||
var ls = new string[] { "\r\n", "\n" };
|
var ls = new string[] { "\r\n", "\n" };
|
||||||
foreach (var item in message.Split(ls, StringSplitOptions.None))
|
foreach (var item in message.Split(ls, StringSplitOptions.None))
|
||||||
Console.WriteLine($"{ts:u} [{source}] {item}");
|
Console.WriteLine($"{ts:s} [{source}] {item}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnCancelKeyPressed(object? sender, ConsoleCancelEventArgs e) {
|
private static void OnCancelKeyPressed(object? sender, ConsoleCancelEventArgs e) {
|
||||||
|
|
|
@ -81,10 +81,7 @@ public sealed class ShardInstance : IDisposable {
|
||||||
private Task Client_Log(LogMessage arg) {
|
private Task Client_Log(LogMessage arg) {
|
||||||
// Suppress certain messages
|
// Suppress certain messages
|
||||||
if (arg.Message != null) {
|
if (arg.Message != null) {
|
||||||
// These warnings appear often as of Discord.Net v3...
|
switch (arg.Message) {
|
||||||
if (arg.Message.StartsWith("Unknown Dispatch ") || arg.Message.StartsWith("Unknown Channel")) return Task.CompletedTask;
|
|
||||||
switch (arg.Message) // Connection status messages replaced by ShardManager's output
|
|
||||||
{
|
|
||||||
case "Connecting":
|
case "Connecting":
|
||||||
case "Connected":
|
case "Connected":
|
||||||
case "Ready":
|
case "Ready":
|
||||||
|
@ -119,14 +116,19 @@ public sealed class ShardInstance : IDisposable {
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
// Update slash/interaction commands
|
// Update slash/interaction commands
|
||||||
if (ShardId == 0) {
|
if (ShardId == 0) {
|
||||||
await _interactionService.RegisterCommandsGloballyAsync(true).ConfigureAwait(false);
|
await _interactionService.RegisterCommandsGloballyAsync(true);
|
||||||
Log(nameof(ShardInstance), "Updated global command registration.");
|
Log(nameof(ShardInstance), "Updated global command registration.");
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Debug: Register our commands locally instead, in each guild we're in
|
// Debug: Register our commands locally instead, in each guild we're in
|
||||||
foreach (var g in DiscordClient.Guilds) {
|
if (DiscordClient.Guilds.Count > 5) {
|
||||||
await _interactionService.RegisterCommandsToGuildAsync(g.Id, true).ConfigureAwait(false);
|
Program.Log(nameof(ShardInstance), "Are you debugging in production?! Skipping DEBUG command registration.");
|
||||||
Log(nameof(ShardInstance), $"Updated DEBUG command registration in guild {g.Id}.");
|
return;
|
||||||
|
} else {
|
||||||
|
foreach (var g in DiscordClient.Guilds) {
|
||||||
|
await _interactionService.RegisterCommandsToGuildAsync(g.Id, true).ConfigureAwait(false);
|
||||||
|
Log(nameof(ShardInstance), $"Updated DEBUG command registration in guild {g.Id}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,9 @@ class ShardManager : IDisposable {
|
||||||
TotalShards = Config.ShardTotal,
|
TotalShards = Config.ShardTotal,
|
||||||
LogLevel = LogSeverity.Info,
|
LogLevel = LogSeverity.Info,
|
||||||
DefaultRetryMode = RetryMode.Retry502 | RetryMode.RetryTimeouts,
|
DefaultRetryMode = RetryMode.Retry502 | RetryMode.RetryTimeouts,
|
||||||
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages,
|
||||||
|
SuppressUnknownDispatchWarnings = true,
|
||||||
|
LogGatewayIntentWarnings = false
|
||||||
};
|
};
|
||||||
var services = new ServiceCollection()
|
var services = new ServiceCollection()
|
||||||
.AddSingleton(s => new ShardInstance(this, s, _textCommands))
|
.AddSingleton(s => new ShardInstance(this, s, _textCommands))
|
||||||
|
|
Loading…
Reference in a new issue