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
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
csharp_style_prefer_primary_constructors = true:suggestion

4
.vscode/launch.json vendored
View file

@ -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",

View file

@ -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.";

View file

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

View file

@ -10,7 +10,7 @@ namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Base class for our interaction module classes. Contains common data for use in implementing classes.
/// </summary>
public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
public abstract partial class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
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<SocketInteractionCon
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`.";
private static readonly Regex DateParse1 = new(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$", RegexOptions.Compiled);
private static readonly Regex DateParse2 = new(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$", RegexOptions.Compiled);
[GeneratedRegex(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$")]
private static partial Regex DateParser1();
[GeneratedRegex(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$")]
private static partial Regex DateParser2();
/// <summary>
/// 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.
/// </exception>
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);
}

View file

@ -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,7 +131,7 @@ public class ConfigModule : BotModuleBase {
[SlashCommand("check", HelpCmdCheck)]
public async Task CmdCheck() {
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;
using var db = new BotDatabaseContext();
@ -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;
}));

View file

@ -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<ListItem> list) {
private static MemoryStream ListExportNormal(SocketGuild guild, IEnumerable<ListItem> 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<ListItem> list) {
private static MemoryStream ListExportCsv(SocketGuild guild, IEnumerable<ListItem> list) {
// Output: User ID, Username, Nickname, Month-Day, Month, Day
var result = new MemoryStream();
var writer = new StreamWriter(result, Encoding.UTF8);

View file

@ -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" +

View file

@ -8,7 +8,7 @@ namespace BirthdayBot.BackgroundServices;
class AutoUserDownload : BackgroundService {
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)
=> Shard.DiscordClient.Disconnected += OnDisconnect;
@ -26,15 +26,14 @@ class AutoUserDownload : BackgroundService {
.Select(g => g.Id)
.ToHashSet();
// ...and if the guild contains any user data
IEnumerable<ulong> mustFetch;
HashSet<ulong> 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();

View file

@ -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.
/// </summary>
class BirthdayRoleUpdate : BackgroundService {
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
class BirthdayRoleUpdate(ShardInstance instance) : BackgroundService(instance) {
/// <summary>
/// Processes birthday updates for all available guilds synchronously.
/// </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
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);
}
}

View file

@ -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.

View file

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

View file

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

View file

@ -9,7 +9,9 @@ namespace BirthdayBot;
/// <summary>
/// Loads and holds configuration values.
/// </summary>
class Configuration {
partial class Configuration {
[GeneratedRegex(@"(?<low>\d{1,2})[-,](?<high>\d{1,2})")]
private static partial Regex ShardRangeParser();
const string KeyShardRange = "ShardRange";
public string BotToken { get; }
@ -70,8 +72,7 @@ class Configuration {
var shardRangeInput = args.ShardRange ?? ReadConfKey<string>(jc, KeyShardRange, false);
if (!string.IsNullOrWhiteSpace(shardRangeInput)) {
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\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);

View file

@ -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
```

View file

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