diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..677ed3f --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.4", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 0b8c1b3..c6ff09e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,13 +13,13 @@ tab_width = 4 # New line preferences end_of_line = crlf -insert_final_newline = false +insert_final_newline = true #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false +dotnet_sort_system_directives_first = true file_header_template = unset # this. and Me. preferences @@ -63,7 +63,7 @@ dotnet_style_prefer_simplified_interpolation = true dotnet_style_readonly_field = true # Parameter preferences -dotnet_code_quality_unused_parameters = all +dotnet_code_quality_unused_parameters = true # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none @@ -77,7 +77,7 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true # var preferences csharp_style_var_elsewhere = false:silent csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent @@ -101,7 +101,6 @@ csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences csharp_prefer_static_local_function = true:suggestion -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async # Code-block preferences csharp_prefer_braces = when_multiline:silent @@ -221,38 +220,4 @@ csharp_style_prefer_local_over_anonymous_function = true:suggestion csharp_style_prefer_tuple_swap = true:suggestion csharp_style_prefer_extended_property_pattern = true:suggestion -[*.{cs,vb}] -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_readonly_field = true:suggestion -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent -dotnet_code_quality_unused_parameters = all:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent \ No newline at end of file +csharp_style_prefer_primary_constructors = true:suggestion diff --git a/.vscode/launch.json b/.vscode/launch.json index 23c8edb..42ff7f8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,8 +10,8 @@ "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": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/settings.json" ], + "program": "${workspaceFolder}/bin/Debug/net8.0/BirthdayBot.dll", + "args": [ "-c", "${workspaceFolder}/bin/Debug/settings.json" ], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", diff --git a/ApplicationCommands/BirthdayModule.cs b/ApplicationCommands/BirthdayModule.cs index 43429ef..2f5f670 100644 --- a/ApplicationCommands/BirthdayModule.cs +++ b/ApplicationCommands/BirthdayModule.cs @@ -4,7 +4,7 @@ using System.Text; namespace BirthdayBot.ApplicationCommands; [Group("birthday", HelpCmdBirthday)] -[EnabledInDm(false)] +[CommandContextType(InteractionContextType.Guild)] public class BirthdayModule : BotModuleBase { public const string HelpCmdBirthday = "Commands relating to birthdays."; public const string HelpCmdSetDate = "Sets or updates your birthday."; diff --git a/ApplicationCommands/BirthdayOverrideModule.cs b/ApplicationCommands/BirthdayOverrideModule.cs index 38c199d..c261f94 100644 --- a/ApplicationCommands/BirthdayOverrideModule.cs +++ b/ApplicationCommands/BirthdayOverrideModule.cs @@ -5,7 +5,7 @@ using static BirthdayBot.Common; namespace BirthdayBot.ApplicationCommands; [Group("override", HelpCmdOverride)] [DefaultMemberPermissions(GuildPermission.ManageGuild)] -[EnabledInDm(false)] +[CommandContextType(InteractionContextType.Guild)] public class BirthdayOverrideModule : BotModuleBase { public const string HelpCmdOverride = "Commands to set options for other users."; const string HelpOptOvTarget = "The user whose data to modify."; @@ -14,8 +14,8 @@ public class BirthdayOverrideModule : BotModuleBase { // TODO possible to use a common base class for shared functionality instead? [SlashCommand("set-birthday", "Set a user's birthday on their behalf.")] - public async Task OvSetBirthday([Summary(description: HelpOptOvTarget)]SocketGuildUser target, - [Summary(description: HelpOptDate)]string date) { + public async Task OvSetBirthday([Summary(description: HelpOptOvTarget)] SocketGuildUser target, + [Summary(description: HelpOptDate)] string date) { int inmonth, inday; try { (inmonth, inday) = ParseDate(date); @@ -43,8 +43,8 @@ public class BirthdayOverrideModule : BotModuleBase { } [SlashCommand("set-timezone", "Set a user's time zone on their behalf.")] - public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target, - [Summary(description: HelpOptZone)]string zone) { + public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)] SocketGuildUser target, + [Summary(description: HelpOptZone)] string zone) { using var db = new BotDatabaseContext(); var user = target.GetUserEntryOrNew(db); @@ -68,7 +68,7 @@ public class BirthdayOverrideModule : BotModuleBase { } [SlashCommand("remove-birthday", "Remove a user's birthday information on their behalf.")] - public async Task OvRemove([Summary(description: HelpOptOvTarget)]SocketGuildUser target) { + public async Task OvRemove([Summary(description: HelpOptOvTarget)] SocketGuildUser target) { using var db = new BotDatabaseContext(); var user = target.GetUserEntryOrNew(db); if (!user.IsNew) { diff --git a/ApplicationCommands/BotModuleBase.cs b/ApplicationCommands/BotModuleBase.cs index 2a6ddd1..7660576 100644 --- a/ApplicationCommands/BotModuleBase.cs +++ b/ApplicationCommands/BotModuleBase.cs @@ -10,7 +10,7 @@ namespace BirthdayBot.ApplicationCommands; /// /// Base class for our interaction module classes. Contains common data for use in implementing classes. /// -public abstract class BotModuleBase : InteractionModuleBase { +public abstract partial class BotModuleBase : InteractionModuleBase { protected const string MemberCacheEmptyError = ":warning: Please try the command again."; public const string AccessDeniedError = ":warning: You are not allowed to run this command."; @@ -65,8 +65,10 @@ public abstract class BotModuleBase : InteractionModuleBase\d{1,2})[ -](?[A-Za-z]+)$", RegexOptions.Compiled); - private static readonly Regex DateParse2 = new(@"^(?[A-Za-z]+)[ -](?\d{1,2})$", RegexOptions.Compiled); + [GeneratedRegex(@"^(?\d{1,2})[ -](?[A-Za-z]+)$")] + private static partial Regex DateParser1(); + [GeneratedRegex(@"^(?[A-Za-z]+)[ -](?\d{1,2})$")] + private static partial Regex DateParser2(); /// /// Parses a date input. @@ -76,10 +78,10 @@ public abstract class BotModuleBase : InteractionModuleBase protected static (int, int) ParseDate(string dateInput) { - var m = DateParse1.Match(dateInput); + var m = DateParser1().Match(dateInput); if (!m.Success) { // Flip the fields around, try again - m = DateParse2.Match(dateInput); + m = DateParser2().Match(dateInput); if (!m.Success) throw new FormatException(FormatError); } diff --git a/ApplicationCommands/ConfigModule.cs b/ApplicationCommands/ConfigModule.cs index 2ce338a..81c55a1 100644 --- a/ApplicationCommands/ConfigModule.cs +++ b/ApplicationCommands/ConfigModule.cs @@ -5,7 +5,7 @@ using System.Text; namespace BirthdayBot.ApplicationCommands; [Group("config", HelpCmdConfig)] [DefaultMemberPermissions(GuildPermission.ManageGuild)] -[EnabledInDm(false)] +[CommandContextType(InteractionContextType.Guild)] public class ConfigModule : BotModuleBase { public const string HelpCmdConfig = "Configure basic settings for the bot."; public const string HelpCmdAnnounce = "Settings regarding birthday announcements."; @@ -112,7 +112,7 @@ public class ConfigModule : BotModuleBase { } [SlashCommand("set-ping", HelpSubCmdPing)] - public async Task CmdSetPing([Summary(description: "Set True to ping users, False to display them normally.")]bool option) { + public async Task CmdSetPing([Summary(description: "Set True to ping users, False to display them normally.")] bool option) { await DoDatabaseUpdate(Context, s => s.AnnouncePing = option); await RespondAsync($":white_check_mark: Announcement pings are now **{(option ? "on" : "off")}**.").ConfigureAwait(false); } @@ -131,8 +131,8 @@ public class ConfigModule : BotModuleBase { [SlashCommand("check", HelpCmdCheck)] public async Task CmdCheck() { static string DoTestFor(string label, Func test) - => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }"; - + => $"{label}: {(test() ? ":white_check_mark: Yes" : ":x: No")}"; + var guild = Context.Guild; using var db = new BotDatabaseContext(); var guildconf = guild.GetConfigOrNew(db); @@ -141,8 +141,8 @@ public class ConfigModule : BotModuleBase { var result = new StringBuilder(); result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{Shard.ShardId:00}`"); - result.AppendLine($"Number of registered birthdays: `{ guildconf.UserEntries.Count }`"); - result.AppendLine($"Server time zone: `{ guildconf.GuildTimeZone ?? "Not set - using UTC" }`"); + result.AppendLine($"Number of registered birthdays: `{guildconf.UserEntries?.Count ?? 0}`"); + result.AppendLine($"Server time zone: `{guildconf.GuildTimeZone ?? "Not set - using UTC"}`"); result.AppendLine(); var hasMembers = Common.HasMostMembersDownloaded(guild); @@ -152,7 +152,8 @@ public class ConfigModule : BotModuleBase { result.Append(DoTestFor("Birthday processing", delegate { if (!hasMembers) return false; if (guildconf.IsNew) return false; - bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(guildconf.UserEntries, guildconf.GuildTimeZone).Count; + bdayCount = BackgroundServices.BirthdayRoleUpdate + .GetGuildCurrentBirthdays(guildconf.UserEntries!, guildconf.GuildTimeZone).Count; return true; })); if (!hasMembers) result.AppendLine(" - Previous step failed."); @@ -180,7 +181,7 @@ public class ConfigModule : BotModuleBase { return announcech != null; })); var disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>"; - result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate { + result.AppendLine(DoTestFor($"(Optional) Bot can send messages into {disp}", delegate { if (announcech == null) return false; return guild.CurrentUser.GetPermissions(announcech).SendMessages; })); diff --git a/ApplicationCommands/ExportModule.cs b/ApplicationCommands/ExportModule.cs index 815ac4c..365f185 100644 --- a/ApplicationCommands/ExportModule.cs +++ b/ApplicationCommands/ExportModule.cs @@ -7,7 +7,7 @@ public class ExportModule : BotModuleBase { [SlashCommand("export-birthdays", HelpCmdExport)] [DefaultMemberPermissions(GuildPermission.ManageGuild)] - [EnabledInDm(false)] + [CommandContextType(InteractionContextType.Guild)] public async Task CmdExport([Summary(description: "Specify whether to export the list in CSV format.")] bool asCsv = false) { if (!await HasMemberCacheAsync(Context.Guild)) { await RespondAsync(MemberCacheEmptyError, ephemeral: true); @@ -28,7 +28,7 @@ public class ExportModule : BotModuleBase { await RespondWithFileAsync(fileoutput, filename, text: $"Exported {bdlist.Count} birthdays to file."); } - private static Stream ListExportNormal(SocketGuild guild, IEnumerable list) { + private static MemoryStream ListExportNormal(SocketGuild guild, IEnumerable list) { // Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]" var result = new MemoryStream(); var writer = new StreamWriter(result, Encoding.UTF8); @@ -52,7 +52,7 @@ public class ExportModule : BotModuleBase { return result; } - private static Stream ListExportCsv(SocketGuild guild, IEnumerable list) { + private static MemoryStream ListExportCsv(SocketGuild guild, IEnumerable list) { // Output: User ID, Username, Nickname, Month-Day, Month, Day var result = new MemoryStream(); var writer = new StreamWriter(result, Encoding.UTF8); diff --git a/ApplicationCommands/HelpModule.cs b/ApplicationCommands/HelpModule.cs index 3da4bb2..fc5a313 100644 --- a/ApplicationCommands/HelpModule.cs +++ b/ApplicationCommands/HelpModule.cs @@ -1,7 +1,7 @@ using Discord.Interactions; namespace BirthdayBot.ApplicationCommands; -[EnabledInDm(true)] +[CommandContextType(InteractionContextType.Guild, InteractionContextType.BotDm)] public class HelpModule : BotModuleBase { private const string TopMessage = "Thank you for using Birthday Bot!\n" + diff --git a/BackgroundServices/AutoUserDownload.cs b/BackgroundServices/AutoUserDownload.cs index a9f8e15..cae178c 100644 --- a/BackgroundServices/AutoUserDownload.cs +++ b/BackgroundServices/AutoUserDownload.cs @@ -8,7 +8,7 @@ namespace BirthdayBot.BackgroundServices; class AutoUserDownload : BackgroundService { private static readonly TimeSpan RequestTimeout = ShardManager.DeadShardThreshold / 3; - private readonly HashSet _skippedGuilds = new(); + private readonly HashSet _skippedGuilds = []; public AutoUserDownload(ShardInstance instance) : base(instance) => Shard.DiscordClient.Disconnected += OnDisconnect; @@ -26,21 +26,20 @@ class AutoUserDownload : BackgroundService { .Select(g => g.Id) .ToHashSet(); // ...and if the guild contains any user data - IEnumerable mustFetch; + HashSet mustFetch; try { await ConcurrentSemaphore.WaitAsync(token); using var db = new BotDatabaseContext(); - mustFetch = db.UserEntries.AsNoTracking() + mustFetch = [.. db.UserEntries.AsNoTracking() .Where(e => incompleteCaches.Contains(e.GuildId)) .Select(e => e.GuildId) - .Where(e => !_skippedGuilds.Contains(e)) - .ToHashSet(); + .Where(e => !_skippedGuilds.Contains(e))]; } finally { try { ConcurrentSemaphore.Release(); } catch (ObjectDisposedException) { } } - + var processed = 0; var processStartTime = DateTimeOffset.UtcNow; foreach (var item in mustFetch) { @@ -67,7 +66,7 @@ class AutoUserDownload : BackgroundService { break; } else if (!dl.IsCompletedSuccessfully) { Log($"Task unresponsive, will skip (ID {guild.Id}, with {guild.MemberCount} members)."); - _skippedGuilds.Add(guild.Id); + _skippedGuilds.Add(guild.Id); continue; } } diff --git a/BackgroundServices/BirthdayRoleUpdate.cs b/BackgroundServices/BirthdayRoleUpdate.cs index ec11c1a..02f01c6 100644 --- a/BackgroundServices/BirthdayRoleUpdate.cs +++ b/BackgroundServices/BirthdayRoleUpdate.cs @@ -7,9 +7,7 @@ namespace BirthdayBot.BackgroundServices; /// Core automatic functionality of the bot. Manages role memberships based on birthday information, /// and optionally sends the announcement message to appropriate guilds. /// -class BirthdayRoleUpdate : BackgroundService { - public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { } - +class BirthdayRoleUpdate(ShardInstance instance) : BackgroundService(instance) { /// /// Processes birthday updates for all available guilds synchronously. /// @@ -94,7 +92,7 @@ class BirthdayRoleUpdate : BackgroundService { // Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar if (!DateTime.IsLeapYear(checkNow.Year) && record.BirthMonth == 2 && record.BirthDay == 29) { if (checkNow.Month == 3 && checkNow.Day == 1) birthdayUsers.Add(record.UserId); - } else if (record.BirthMonth == checkNow.Month && record.BirthDay== checkNow.Day) { + } else if (record.BirthMonth == checkNow.Month && record.BirthDay == checkNow.Day) { birthdayUsers.Add(record.UserId); } } diff --git a/BackgroundServices/DataRetention.cs b/BackgroundServices/DataRetention.cs index 76b1b83..76f4ace 100644 --- a/BackgroundServices/DataRetention.cs +++ b/BackgroundServices/DataRetention.cs @@ -13,9 +13,8 @@ class DataRetention : BackgroundService { const int StaleGuildThreshold = 180; const int StaleUserThreashold = 360; - public DataRetention(ShardInstance instance) : base(instance) { - ProcessInterval = 21600 / Shard.Config.BackgroundInterval; // Process about once per six hours - } + public DataRetention(ShardInstance instance) : base(instance) + => ProcessInterval = 21600 / Shard.Config.BackgroundInterval; // Process about once per six hours public override async Task OnTick(int tickCount, CancellationToken token) { // Run only a subset of shards each time, each running every ProcessInterval ticks. @@ -60,7 +59,7 @@ class DataRetention : BackgroundService { .Where(gu => localGuilds.Contains(gu.GuildId)) .Where(gu => now - TimeSpan.FromDays(StaleUserThreashold) > gu.LastSeen) .ExecuteDeleteAsync(); - + // Build report var resultText = new StringBuilder(); resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users."); diff --git a/BirthdayBot.csproj b/BirthdayBot.csproj index fa95ff5..94762a6 100644 --- a/BirthdayBot.csproj +++ b/BirthdayBot.csproj @@ -1,12 +1,14 @@  + 3.5.3 + NoiTheCat + Exe - net6.0 + net8.0 enable enable - 3.5.2 - NoiTheCat + en @@ -22,17 +24,17 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + diff --git a/Common.cs b/Common.cs index 9096c98..3b10f22 100644 --- a/Common.cs +++ b/Common.cs @@ -21,8 +21,8 @@ static class Common { if (member.DiscriminatorValue == 0) { var username = escapeFormattingCharacters(member.GlobalName ?? member.Username); - if (member.Nickname != null) { - return $"{escapeFormattingCharacters(member.Nickname)} ({username})"; + if (member.Nickname != null) { + return $"{escapeFormattingCharacters(member.Nickname)} ({username})"; } return username; } else { diff --git a/Configuration.cs b/Configuration.cs index ac1f04b..a8fdb63 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -9,7 +9,9 @@ namespace BirthdayBot; /// /// Loads and holds configuration values. /// -class Configuration { +partial class Configuration { + [GeneratedRegex(@"(?\d{1,2})[-,](?\d{1,2})")] + private static partial Regex ShardRangeParser(); const string KeyShardRange = "ShardRange"; public string BotToken { get; } @@ -18,7 +20,7 @@ class Configuration { public int ShardStart { get; } public int ShardAmount { get; } public int ShardTotal { get; } - + public string? SqlHost { get; } public string? SqlDatabase { get; } public string SqlUsername { get; } @@ -70,8 +72,7 @@ class Configuration { var shardRangeInput = args.ShardRange ?? ReadConfKey(jc, KeyShardRange, false); if (!string.IsNullOrWhiteSpace(shardRangeInput)) { - Regex srPicker = new(@"(?\d{1,2})[-,]{1}(?\d{1,2})"); - var m = srPicker.Match(shardRangeInput); + var m = ShardRangeParser().Match(shardRangeInput); if (m.Success) { ShardStart = int.Parse(m.Groups["low"].Value); var high = int.Parse(m.Groups["high"].Value); diff --git a/Data/Extensions.cs b/Data/Extensions.cs index 01ad926..8693014 100644 --- a/Data/Extensions.cs +++ b/Data/Extensions.cs @@ -5,7 +5,7 @@ internal static class Extensions { /// If it doesn't exist in the database, returns true. /// public static GuildConfig GetConfigOrNew(this SocketGuild guild, BotDatabaseContext db) - => db.GuildConfigurations.Where(g => g.GuildId == guild.Id).FirstOrDefault() + => db.GuildConfigurations.Where(g => g.GuildId == guild.Id).FirstOrDefault() ?? new GuildConfig() { IsNew = true, GuildId = guild.Id }; /// diff --git a/Data/GuildConfig.cs b/Data/GuildConfig.cs index a4cfb1d..09569cd 100644 --- a/Data/GuildConfig.cs +++ b/Data/GuildConfig.cs @@ -21,7 +21,7 @@ public class GuildConfig { public string? AnnounceMessagePl { get; set; } public bool AnnouncePing { get; set; } - + public DateTimeOffset LastSeen { get; set; } [InverseProperty(nameof(UserEntry.Guild))] diff --git a/Readme.md b/Readme.md index 423731f..08f7847 100644 --- a/Readme.md +++ b/Readme.md @@ -9,9 +9,8 @@ An automated way to recognize birthdays in your community! #### Running your own instance You need: -* .NET 6 (https://dotnet.microsoft.com/en-us/) +* .NET 8 (https://dotnet.microsoft.com/en-us/) * PostgreSQL (https://www.postgresql.org/) -* EF Core tools (https://learn.microsoft.com/en-us/ef/core/get-started/overview/install#get-the-entity-framework-core-tools) * A Discord bot token (https://discord.com/developers/applications) Get your bot token and set up your database user and schema, then create a JSON file containing the following: @@ -28,10 +27,11 @@ Get your bot token and set up your database user and schema, then create a JSON Then run the following commands: ```sh $ dotnet restore +$ dotnet tool restore $ dotnet ef database update -- -c path/to/config.json ``` And finally, to run the bot: -``` +```sh $ dotnet run -c Release -- -c path/to/config.json ``` \ No newline at end of file diff --git a/ShardManager.cs b/ShardManager.cs index 5cf4612..f6211df 100644 --- a/ShardManager.cs +++ b/ShardManager.cs @@ -36,7 +36,7 @@ class ShardManager : IDisposable { Config = cfg; // Allocate shards based on configuration - _shards = new Dictionary(); + _shards = []; for (var i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) { _shards.Add(i, null); } @@ -128,7 +128,7 @@ class ShardManager : IDisposable { } else { shardStatuses.Append('.'); } - + shardStatuses.AppendLine(); if (lastRun > DeadShardThreshold) {