mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-21 13:54:36 +00:00
commit
1f6e7cab1d
19 changed files with 83 additions and 104 deletions
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.4",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
4
.vscode/launch.json
vendored
|
@ -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",
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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" +
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))]
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue