Merge pull request #57 from NoiTheCat/dev/net8

Upgrade to .NET 8
This commit is contained in:
Noi 2024-05-09 22:40:33 -07:00 committed by GitHub
commit 1f6e7cab1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 83 additions and 104 deletions

12
.config/dotnet-tools.json Normal file
View file

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.4",
"commands": [
"dotnet-ef"
]
}
}
}

View file

@ -13,13 +13,13 @@ tab_width = 4
# New line preferences # New line preferences
end_of_line = crlf end_of_line = crlf
insert_final_newline = false insert_final_newline = true
#### .NET Coding Conventions #### #### .NET Coding Conventions ####
# Organize usings # Organize usings
dotnet_separate_import_directive_groups = false dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false dotnet_sort_system_directives_first = true
file_header_template = unset file_header_template = unset
# this. and Me. preferences # this. and Me. preferences
@ -63,7 +63,7 @@ dotnet_style_prefer_simplified_interpolation = true
dotnet_style_readonly_field = true dotnet_style_readonly_field = true
# Parameter preferences # Parameter preferences
dotnet_code_quality_unused_parameters = all dotnet_code_quality_unused_parameters = true
# Suppression preferences # Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none dotnet_remove_unnecessary_suppression_exclusions = none
@ -77,7 +77,7 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true
# var preferences # var preferences
csharp_style_var_elsewhere = false:silent csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = true:suggestion 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 # Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_accessors = true:silent
@ -101,7 +101,6 @@ csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences # Modifier preferences
csharp_prefer_static_local_function = true:suggestion 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 # Code-block preferences
csharp_prefer_braces = when_multiline:silent 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_tuple_swap = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion csharp_style_prefer_extended_property_pattern = true:suggestion
[*.{cs,vb}] csharp_style_prefer_primary_constructors = true:suggestion
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

4
.vscode/launch.json vendored
View file

@ -10,8 +10,8 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net6.0/BirthdayBot.dll", "program": "${workspaceFolder}/bin/Debug/net8.0/BirthdayBot.dll",
"args": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/settings.json" ], "args": [ "-c", "${workspaceFolder}/bin/Debug/settings.json" ],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole", "console": "internalConsole",

View file

@ -4,7 +4,7 @@ using System.Text;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[Group("birthday", HelpCmdBirthday)] [Group("birthday", HelpCmdBirthday)]
[EnabledInDm(false)] [CommandContextType(InteractionContextType.Guild)]
public class BirthdayModule : BotModuleBase { public class BirthdayModule : BotModuleBase {
public const string HelpCmdBirthday = "Commands relating to birthdays."; public const string HelpCmdBirthday = "Commands relating to birthdays.";
public const string HelpCmdSetDate = "Sets or updates your birthday."; public const string HelpCmdSetDate = "Sets or updates your birthday.";

View file

@ -5,7 +5,7 @@ using static BirthdayBot.Common;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[Group("override", HelpCmdOverride)] [Group("override", HelpCmdOverride)]
[DefaultMemberPermissions(GuildPermission.ManageGuild)] [DefaultMemberPermissions(GuildPermission.ManageGuild)]
[EnabledInDm(false)] [CommandContextType(InteractionContextType.Guild)]
public class BirthdayOverrideModule : BotModuleBase { public class BirthdayOverrideModule : BotModuleBase {
public const string HelpCmdOverride = "Commands to set options for other users."; public const string HelpCmdOverride = "Commands to set options for other users.";
const string HelpOptOvTarget = "The user whose data to modify."; 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? // TODO possible to use a common base class for shared functionality instead?
[SlashCommand("set-birthday", "Set a user's birthday on their behalf.")] [SlashCommand("set-birthday", "Set a user's birthday on their behalf.")]
public async Task OvSetBirthday([Summary(description: HelpOptOvTarget)]SocketGuildUser target, public async Task OvSetBirthday([Summary(description: HelpOptOvTarget)] SocketGuildUser target,
[Summary(description: HelpOptDate)]string date) { [Summary(description: HelpOptDate)] string date) {
int inmonth, inday; int inmonth, inday;
try { try {
(inmonth, inday) = ParseDate(date); (inmonth, inday) = ParseDate(date);
@ -43,8 +43,8 @@ public class BirthdayOverrideModule : BotModuleBase {
} }
[SlashCommand("set-timezone", "Set a user's time zone on their behalf.")] [SlashCommand("set-timezone", "Set a user's time zone on their behalf.")]
public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)]SocketGuildUser target, public async Task OvSetTimezone([Summary(description: HelpOptOvTarget)] SocketGuildUser target,
[Summary(description: HelpOptZone)]string zone) { [Summary(description: HelpOptZone)] string zone) {
using var db = new BotDatabaseContext(); using var db = new BotDatabaseContext();
var user = target.GetUserEntryOrNew(db); 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.")] [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(); using var db = new BotDatabaseContext();
var user = target.GetUserEntryOrNew(db); var user = target.GetUserEntryOrNew(db);
if (!user.IsNew) { if (!user.IsNew) {

View file

@ -10,7 +10,7 @@ namespace BirthdayBot.ApplicationCommands;
/// <summary> /// <summary>
/// Base class for our interaction module classes. Contains common data for use in implementing classes. /// Base class for our interaction module classes. Contains common data for use in implementing classes.
/// </summary> /// </summary>
public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> { public abstract partial class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
protected const string MemberCacheEmptyError = ":warning: Please try the command again."; protected const string MemberCacheEmptyError = ":warning: Please try the command again.";
public const string AccessDeniedError = ":warning: You are not allowed to run this command."; public const string AccessDeniedError = ":warning: You are not allowed to run this command.";
@ -65,8 +65,10 @@ public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionCon
const string FormatError = ":x: Unrecognized date format. The following formats are accepted, as examples: " const string FormatError = ":x: Unrecognized date format. The following formats are accepted, as examples: "
+ "`15-jan`, `jan-15`, `15 jan`, `jan 15`, `15 January`, `January 15`."; + "`15-jan`, `jan-15`, `15 jan`, `jan 15`, `15 January`, `January 15`.";
private static readonly Regex DateParse1 = new(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$", RegexOptions.Compiled); [GeneratedRegex(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$")]
private static readonly Regex DateParse2 = new(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$", RegexOptions.Compiled); private static partial Regex DateParser1();
[GeneratedRegex(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$")]
private static partial Regex DateParser2();
/// <summary> /// <summary>
/// Parses a date input. /// Parses a date input.
@ -76,10 +78,10 @@ public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionCon
/// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is. /// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is.
/// </exception> /// </exception>
protected static (int, int) ParseDate(string dateInput) { protected static (int, int) ParseDate(string dateInput) {
var m = DateParse1.Match(dateInput); var m = DateParser1().Match(dateInput);
if (!m.Success) { if (!m.Success) {
// Flip the fields around, try again // Flip the fields around, try again
m = DateParse2.Match(dateInput); m = DateParser2().Match(dateInput);
if (!m.Success) throw new FormatException(FormatError); if (!m.Success) throw new FormatException(FormatError);
} }

View file

@ -5,7 +5,7 @@ using System.Text;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[Group("config", HelpCmdConfig)] [Group("config", HelpCmdConfig)]
[DefaultMemberPermissions(GuildPermission.ManageGuild)] [DefaultMemberPermissions(GuildPermission.ManageGuild)]
[EnabledInDm(false)] [CommandContextType(InteractionContextType.Guild)]
public class ConfigModule : BotModuleBase { public class ConfigModule : BotModuleBase {
public const string HelpCmdConfig = "Configure basic settings for the bot."; public const string HelpCmdConfig = "Configure basic settings for the bot.";
public const string HelpCmdAnnounce = "Settings regarding birthday announcements."; public const string HelpCmdAnnounce = "Settings regarding birthday announcements.";
@ -112,7 +112,7 @@ public class ConfigModule : BotModuleBase {
} }
[SlashCommand("set-ping", HelpSubCmdPing)] [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 DoDatabaseUpdate(Context, s => s.AnnouncePing = option);
await RespondAsync($":white_check_mark: Announcement pings are now **{(option ? "on" : "off")}**.").ConfigureAwait(false); 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)] [SlashCommand("check", HelpCmdCheck)]
public async Task CmdCheck() { public async Task CmdCheck() {
static string DoTestFor(string label, Func<bool> test) static string DoTestFor(string label, Func<bool> test)
=> $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }"; => $"{label}: {(test() ? ":white_check_mark: Yes" : ":x: No")}";
var guild = Context.Guild; var guild = Context.Guild;
using var db = new BotDatabaseContext(); using var db = new BotDatabaseContext();
var guildconf = guild.GetConfigOrNew(db); var guildconf = guild.GetConfigOrNew(db);
@ -141,8 +141,8 @@ public class ConfigModule : BotModuleBase {
var result = new StringBuilder(); var result = new StringBuilder();
result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{Shard.ShardId:00}`"); result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{Shard.ShardId:00}`");
result.AppendLine($"Number of registered birthdays: `{ guildconf.UserEntries.Count }`"); result.AppendLine($"Number of registered birthdays: `{guildconf.UserEntries?.Count ?? 0}`");
result.AppendLine($"Server time zone: `{ guildconf.GuildTimeZone ?? "Not set - using UTC" }`"); result.AppendLine($"Server time zone: `{guildconf.GuildTimeZone ?? "Not set - using UTC"}`");
result.AppendLine(); result.AppendLine();
var hasMembers = Common.HasMostMembersDownloaded(guild); var hasMembers = Common.HasMostMembersDownloaded(guild);
@ -152,7 +152,8 @@ public class ConfigModule : BotModuleBase {
result.Append(DoTestFor("Birthday processing", delegate { result.Append(DoTestFor("Birthday processing", delegate {
if (!hasMembers) return false; if (!hasMembers) return false;
if (guildconf.IsNew) 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; return true;
})); }));
if (!hasMembers) result.AppendLine(" - Previous step failed."); if (!hasMembers) result.AppendLine(" - Previous step failed.");
@ -180,7 +181,7 @@ public class ConfigModule : BotModuleBase {
return announcech != null; return announcech != null;
})); }));
var disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>"; 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; if (announcech == null) return false;
return guild.CurrentUser.GetPermissions(announcech).SendMessages; return guild.CurrentUser.GetPermissions(announcech).SendMessages;
})); }));

View file

@ -7,7 +7,7 @@ public class ExportModule : BotModuleBase {
[SlashCommand("export-birthdays", HelpCmdExport)] [SlashCommand("export-birthdays", HelpCmdExport)]
[DefaultMemberPermissions(GuildPermission.ManageGuild)] [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) { public async Task CmdExport([Summary(description: "Specify whether to export the list in CSV format.")] bool asCsv = false) {
if (!await HasMemberCacheAsync(Context.Guild)) { if (!await HasMemberCacheAsync(Context.Guild)) {
await RespondAsync(MemberCacheEmptyError, ephemeral: true); await RespondAsync(MemberCacheEmptyError, ephemeral: true);
@ -28,7 +28,7 @@ public class ExportModule : BotModuleBase {
await RespondWithFileAsync(fileoutput, filename, text: $"Exported {bdlist.Count} birthdays to file."); await RespondWithFileAsync(fileoutput, filename, text: $"Exported {bdlist.Count} birthdays to file.");
} }
private static Stream ListExportNormal(SocketGuild guild, IEnumerable<ListItem> list) { private static MemoryStream ListExportNormal(SocketGuild guild, IEnumerable<ListItem> list) {
// Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]" // Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]"
var result = new MemoryStream(); var result = new MemoryStream();
var writer = new StreamWriter(result, Encoding.UTF8); var writer = new StreamWriter(result, Encoding.UTF8);
@ -52,7 +52,7 @@ public class ExportModule : BotModuleBase {
return result; return result;
} }
private static Stream ListExportCsv(SocketGuild guild, IEnumerable<ListItem> list) { private static MemoryStream ListExportCsv(SocketGuild guild, IEnumerable<ListItem> list) {
// Output: User ID, Username, Nickname, Month-Day, Month, Day // Output: User ID, Username, Nickname, Month-Day, Month, Day
var result = new MemoryStream(); var result = new MemoryStream();
var writer = new StreamWriter(result, Encoding.UTF8); var writer = new StreamWriter(result, Encoding.UTF8);

View file

@ -1,7 +1,7 @@
using Discord.Interactions; using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[EnabledInDm(true)] [CommandContextType(InteractionContextType.Guild, InteractionContextType.BotDm)]
public class HelpModule : BotModuleBase { public class HelpModule : BotModuleBase {
private const string TopMessage = private const string TopMessage =
"Thank you for using Birthday Bot!\n" + "Thank you for using Birthday Bot!\n" +

View file

@ -8,7 +8,7 @@ namespace BirthdayBot.BackgroundServices;
class AutoUserDownload : BackgroundService { class AutoUserDownload : BackgroundService {
private static readonly TimeSpan RequestTimeout = ShardManager.DeadShardThreshold / 3; private static readonly TimeSpan RequestTimeout = ShardManager.DeadShardThreshold / 3;
private readonly HashSet<ulong> _skippedGuilds = new(); private readonly HashSet<ulong> _skippedGuilds = [];
public AutoUserDownload(ShardInstance instance) : base(instance) public AutoUserDownload(ShardInstance instance) : base(instance)
=> Shard.DiscordClient.Disconnected += OnDisconnect; => Shard.DiscordClient.Disconnected += OnDisconnect;
@ -26,21 +26,20 @@ class AutoUserDownload : BackgroundService {
.Select(g => g.Id) .Select(g => g.Id)
.ToHashSet(); .ToHashSet();
// ...and if the guild contains any user data // ...and if the guild contains any user data
IEnumerable<ulong> mustFetch; HashSet<ulong> mustFetch;
try { try {
await ConcurrentSemaphore.WaitAsync(token); await ConcurrentSemaphore.WaitAsync(token);
using var db = new BotDatabaseContext(); using var db = new BotDatabaseContext();
mustFetch = db.UserEntries.AsNoTracking() mustFetch = [.. db.UserEntries.AsNoTracking()
.Where(e => incompleteCaches.Contains(e.GuildId)) .Where(e => incompleteCaches.Contains(e.GuildId))
.Select(e => e.GuildId) .Select(e => e.GuildId)
.Where(e => !_skippedGuilds.Contains(e)) .Where(e => !_skippedGuilds.Contains(e))];
.ToHashSet();
} finally { } finally {
try { try {
ConcurrentSemaphore.Release(); ConcurrentSemaphore.Release();
} catch (ObjectDisposedException) { } } catch (ObjectDisposedException) { }
} }
var processed = 0; var processed = 0;
var processStartTime = DateTimeOffset.UtcNow; var processStartTime = DateTimeOffset.UtcNow;
foreach (var item in mustFetch) { foreach (var item in mustFetch) {
@ -67,7 +66,7 @@ class AutoUserDownload : BackgroundService {
break; break;
} else if (!dl.IsCompletedSuccessfully) { } else if (!dl.IsCompletedSuccessfully) {
Log($"Task unresponsive, will skip (ID {guild.Id}, with {guild.MemberCount} members)."); Log($"Task unresponsive, will skip (ID {guild.Id}, with {guild.MemberCount} members).");
_skippedGuilds.Add(guild.Id); _skippedGuilds.Add(guild.Id);
continue; continue;
} }
} }

View file

@ -7,9 +7,7 @@ namespace BirthdayBot.BackgroundServices;
/// 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.
/// </summary> /// </summary>
class BirthdayRoleUpdate : BackgroundService { class BirthdayRoleUpdate(ShardInstance instance) : BackgroundService(instance) {
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
/// <summary> /// <summary>
/// Processes birthday updates for all available guilds synchronously. /// Processes birthday updates for all available guilds synchronously.
/// </summary> /// </summary>
@ -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 // 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 (!DateTime.IsLeapYear(checkNow.Year) && record.BirthMonth == 2 && record.BirthDay == 29) {
if (checkNow.Month == 3 && checkNow.Day == 1) birthdayUsers.Add(record.UserId); 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); birthdayUsers.Add(record.UserId);
} }
} }

View file

@ -13,9 +13,8 @@ class DataRetention : BackgroundService {
const int StaleGuildThreshold = 180; const int StaleGuildThreshold = 180;
const int StaleUserThreashold = 360; const int StaleUserThreashold = 360;
public DataRetention(ShardInstance instance) : base(instance) { public DataRetention(ShardInstance instance) : base(instance)
ProcessInterval = 21600 / Shard.Config.BackgroundInterval; // Process about once per six hours => ProcessInterval = 21600 / Shard.Config.BackgroundInterval; // Process about once per six hours
}
public override async Task OnTick(int tickCount, CancellationToken token) { public override async Task OnTick(int tickCount, CancellationToken token) {
// Run only a subset of shards each time, each running every ProcessInterval ticks. // 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 => localGuilds.Contains(gu.GuildId))
.Where(gu => now - TimeSpan.FromDays(StaleUserThreashold) > gu.LastSeen) .Where(gu => now - TimeSpan.FromDays(StaleUserThreashold) > gu.LastSeen)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
// Build report // Build report
var resultText = new StringBuilder(); var resultText = new StringBuilder();
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users."); resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");

View file

@ -1,12 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Version>3.5.3</Version>
<Authors>NoiTheCat</Authors>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>3.5.2</Version> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Authors>NoiTheCat</Authors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@ -22,17 +24,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.12.0" /> <PackageReference Include="Discord.Net" Version="3.14.1" />
<PackageReference Include="EFCore.NamingConventions" Version="7.0.2" /> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<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="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NodaTime" Version="3.1.9" /> <PackageReference Include="NodaTime" Version="3.1.11" />
<PackageReference Include="Npgsql" Version="7.0.4" /> <PackageReference Include="Npgsql" Version="8.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
</ItemGroup> </ItemGroup>

View file

@ -21,8 +21,8 @@ static class Common {
if (member.DiscriminatorValue == 0) { if (member.DiscriminatorValue == 0) {
var username = escapeFormattingCharacters(member.GlobalName ?? member.Username); var username = escapeFormattingCharacters(member.GlobalName ?? member.Username);
if (member.Nickname != null) { if (member.Nickname != null) {
return $"{escapeFormattingCharacters(member.Nickname)} ({username})"; return $"{escapeFormattingCharacters(member.Nickname)} ({username})";
} }
return username; return username;
} else { } else {

View file

@ -9,7 +9,9 @@ namespace BirthdayBot;
/// <summary> /// <summary>
/// Loads and holds configuration values. /// Loads and holds configuration values.
/// </summary> /// </summary>
class Configuration { partial class Configuration {
[GeneratedRegex(@"(?<low>\d{1,2})[-,](?<high>\d{1,2})")]
private static partial Regex ShardRangeParser();
const string KeyShardRange = "ShardRange"; const string KeyShardRange = "ShardRange";
public string BotToken { get; } public string BotToken { get; }
@ -18,7 +20,7 @@ class Configuration {
public int ShardStart { get; } public int ShardStart { get; }
public int ShardAmount { get; } public int ShardAmount { get; }
public int ShardTotal { get; } public int ShardTotal { get; }
public string? SqlHost { get; } public string? SqlHost { get; }
public string? SqlDatabase { get; } public string? SqlDatabase { get; }
public string SqlUsername { get; } public string SqlUsername { get; }
@ -70,8 +72,7 @@ class Configuration {
var shardRangeInput = args.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})"); var m = ShardRangeParser().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);
var high = int.Parse(m.Groups["high"].Value); var high = int.Parse(m.Groups["high"].Value);

View file

@ -5,7 +5,7 @@ internal static class Extensions {
/// If it doesn't exist in the database, <see cref="GuildConfig.IsNew"/> returns true. /// If it doesn't exist in the database, <see cref="GuildConfig.IsNew"/> returns true.
/// </summary> /// </summary>
public static GuildConfig GetConfigOrNew(this SocketGuild guild, BotDatabaseContext db) 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 }; ?? new GuildConfig() { IsNew = true, GuildId = guild.Id };
/// <summary> /// <summary>

View file

@ -21,7 +21,7 @@ public class GuildConfig {
public string? AnnounceMessagePl { get; set; } public string? AnnounceMessagePl { get; set; }
public bool AnnouncePing { get; set; } public bool AnnouncePing { get; set; }
public DateTimeOffset LastSeen { get; set; } public DateTimeOffset LastSeen { get; set; }
[InverseProperty(nameof(UserEntry.Guild))] [InverseProperty(nameof(UserEntry.Guild))]

View file

@ -9,9 +9,8 @@ An automated way to recognize birthdays in your community!
#### Running your own instance #### Running your own instance
You need: You need:
* .NET 6 (https://dotnet.microsoft.com/en-us/) * .NET 8 (https://dotnet.microsoft.com/en-us/)
* PostgreSQL (https://www.postgresql.org/) * 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) * 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: 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: Then run the following commands:
```sh ```sh
$ dotnet restore $ dotnet restore
$ dotnet tool restore
$ dotnet ef database update -- -c path/to/config.json $ dotnet ef database update -- -c path/to/config.json
``` ```
And finally, to run the bot: And finally, to run the bot:
``` ```sh
$ dotnet run -c Release -- -c path/to/config.json $ dotnet run -c Release -- -c path/to/config.json
``` ```

View file

@ -36,7 +36,7 @@ class ShardManager : IDisposable {
Config = cfg; Config = cfg;
// Allocate shards based on configuration // Allocate shards based on configuration
_shards = new Dictionary<int, ShardInstance?>(); _shards = [];
for (var i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) { for (var i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) {
_shards.Add(i, null); _shards.Add(i, null);
} }
@ -128,7 +128,7 @@ class ShardManager : IDisposable {
} else { } else {
shardStatuses.Append('.'); shardStatuses.Append('.');
} }
shardStatuses.AppendLine(); shardStatuses.AppendLine();
if (lastRun > DeadShardThreshold) { if (lastRun > DeadShardThreshold) {