From 7668c82cf9f70bf42f0412ae63b76c3849d67ca0 Mon Sep 17 00:00:00 2001 From: Noi Date: Sat, 8 Jun 2024 19:00:10 -0700 Subject: [PATCH 1/3] Switch to .NET 8 * Bump version * Update dependencies * Copy .editorconfig from BirthdayBot * Still want to properly look over it some time * Set up dotnet-ef as project tool --- .config/dotnet-tools.json | 12 +++++++++++ .editorconfig | 45 +++++---------------------------------- .vscode/launch.json | 4 ++-- RegexBot.csproj | 24 +++++++++++---------- 4 files changed, 32 insertions(+), 53 deletions(-) create mode 100644 .config/dotnet-tools.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..b99844e --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.6", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 0b8c1b3..452f49d 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 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index eb527f9..49a830b 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/RegexBot.dll", - "args": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/config.json" ], + "program": "${workspaceFolder}/bin/Debug/net8.0/RegexBot.dll", + "args": [ "-c", "${workspaceFolder}/bin/Debug/config.json" ], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", diff --git a/RegexBot.csproj b/RegexBot.csproj index e1a4cf4..a72b95c 100644 --- a/RegexBot.csproj +++ b/RegexBot.csproj @@ -1,29 +1,31 @@  - Exe - net6.0 + 3.2.1 NoiTheCat Advanced and flexible Discord moderation bot. - 3.2.0 + + Exe + net8.0 enable enable True + en - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + From cc148d5257449c73477825235cecf37fcb6e5d8d Mon Sep 17 00:00:00 2001 From: Noi Date: Sat, 8 Jun 2024 21:07:35 -0700 Subject: [PATCH 2/3] Implement style suggestions --- Common/EntityList.cs | 3 ++- Common/RateLimit.cs | 2 +- Common/Utilities.cs | 25 +++++++++++-------- Configuration.cs | 5 ++-- ModuleLoadException.cs | 7 +----- ModuleLoader.cs | 2 +- Modules/AutoResponder/AutoResponder.cs | 2 +- Modules/EntryRole/EntryRole.cs | 2 +- Modules/EntryRole/GuildData.cs | 2 +- Modules/ModCommands/ModuleConfig.cs | 4 +-- Modules/RegexModerator/ResponseExecutor.cs | 2 +- Modules/VoiceRoleSync/ModuleConfig.cs | 4 +-- RegexbotModule.cs | 12 +++------ .../CommonFunctions/CommonFunctionsService.cs | 3 +-- Services/EntityCache/UserCachingSubservice.cs | 13 +++++----- Services/Logging/LoggingService.cs | 2 +- Services/ModuleState/ModuleStateService.cs | 19 ++++++++------ Services/Service.cs | 6 ++--- 18 files changed, 54 insertions(+), 61 deletions(-) diff --git a/Common/EntityList.cs b/Common/EntityList.cs index 4755a6f..c8a0f02 100644 --- a/Common/EntityList.cs +++ b/Common/EntityList.cs @@ -1,11 +1,12 @@ using System.Collections; +using System.Collections.ObjectModel; namespace RegexBot.Common; /// /// Represents a commonly-used configuration structure: an array of strings consisting of values. /// public class EntityList : IEnumerable { - private readonly IReadOnlyCollection _innerList; + private readonly ReadOnlyCollection _innerList; /// Gets an enumerable collection of all role names defined in this list. public IEnumerable Roles diff --git a/Common/RateLimit.cs b/Common/RateLimit.cs index 72377d8..2ae398e 100644 --- a/Common/RateLimit.cs +++ b/Common/RateLimit.cs @@ -11,7 +11,7 @@ public class RateLimit where T : notnull { /// Time until an entry within this instance expires, in seconds. /// public int Timeout { get; } - private Dictionary Entries { get; } = new Dictionary(); + private Dictionary Entries { get; } = []; /// /// Creates a new instance with the default timeout value. diff --git a/Common/Utilities.cs b/Common/Utilities.cs index f3e96cb..474743d 100644 --- a/Common/Utilities.cs +++ b/Common/Utilities.cs @@ -1,7 +1,6 @@ using Discord; using RegexBot.Data; using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Text; using System.Text.RegularExpressions; @@ -9,26 +8,30 @@ namespace RegexBot.Common; /// /// Miscellaneous utility methods useful for the bot and modules. /// -public static class Utilities { +public static partial class Utilities { /// - /// Gets a compiled regex that matches a channel tag and pulls its snowflake value. + /// Gets a precompiled regex that matches a channel tag and pulls its snowflake value. /// - public static Regex ChannelMention { get; } = new(@"<#(?\d+)>", RegexOptions.Compiled); + [GeneratedRegex(@"<#(?\d+)>")] + public static partial Regex ChannelMentionRegex(); /// - /// Gets a compiled regex that matches a custom emoji and pulls its name and ID. + /// Gets a precompiled regex that matches a custom emoji and pulls its name and ID. /// - public static Regex CustomEmoji { get; } = new(@"<:(?[A-Za-z0-9_]{2,}):(?\d+)>", RegexOptions.Compiled); + [GeneratedRegex(@"<:(?[A-Za-z0-9_]{2,}):(?\d+)>")] + public static partial Regex CustomEmojiRegex(); /// - /// Gets a compiled regex that matches a fully formed Discord handle, extracting the name and discriminator. + /// Gets a precompiled regex that matches a fully formed Discord handle, extracting the name and discriminator. /// - public static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled); + [GeneratedRegex(@"(.+)#(\d{4}(?!\d))")] + public static partial Regex DiscriminatorSearchRegex(); /// - /// Gets a compiled regex that matches a user tag and pulls its snowflake value. + /// Gets a precompiled regex that matches a user tag and pulls its snowflake value. /// - public static Regex UserMention { get; } = new(@"<@!?(?\d+)>", RegexOptions.Compiled); + [GeneratedRegex(@"<@!?(?\d+)>")] + public static partial Regex UserMentionRegex(); /// /// Performs common checks on the specified message to see if it fits all the criteria of a @@ -77,7 +80,7 @@ public static class Utilities { /// If given string is in an EntityName format, returns a displayable representation of it based on /// a cache query. Otherwise, returns the input string as-is. /// - [return: NotNullIfNotNull("input")] + [return: NotNullIfNotNull(nameof(input))] public static string? TryFromEntityNameString(string? input, RegexbotClient bot) { string? result = null; try { diff --git a/Configuration.cs b/Configuration.cs index eef0dbe..75dfbd1 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -51,9 +51,8 @@ class Configuration { throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration."); } - var dbconf = conf["DatabaseOptions"]?.Value(); - if (dbconf == null) throw new Exception("Database settings were not specified in configuration."); - // TODO more detailed database configuration? password file, other advanced authentication settings... look into this. + var dbconf = (conf["DatabaseOptions"]?.Value()) + ?? throw new Exception("Database settings were not specified in configuration."); Host = ReadConfKey(dbconf, nameof(Host), false); Database = ReadConfKey(dbconf, nameof(Database), false); Username = ReadConfKey(dbconf, nameof(Username), true); diff --git a/ModuleLoadException.cs b/ModuleLoadException.cs index 41f6bef..0f859de 100644 --- a/ModuleLoadException.cs +++ b/ModuleLoadException.cs @@ -4,9 +4,4 @@ /// Represents an error occurring when a module attempts to create a new guild state object /// (that is, read or refresh its configuration). /// -public class ModuleLoadException : Exception { - /// - /// Initializes this exception class with the specified error message. - /// - public ModuleLoadException(string message) : base(message) { } -} +public class ModuleLoadException(string message) : Exception(message) { } diff --git a/ModuleLoader.cs b/ModuleLoader.cs index 9bc9e34..a200982 100644 --- a/ModuleLoader.cs +++ b/ModuleLoader.cs @@ -38,7 +38,7 @@ static class ModuleLoader { return modules.AsReadOnly(); } - static IEnumerable LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) { + static List LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) { var eligibleTypes = from type in asm.GetTypes() where !type.IsAssignableFrom(typeof(RegexbotModule)) where type.GetCustomAttribute() != null diff --git a/Modules/AutoResponder/AutoResponder.cs b/Modules/AutoResponder/AutoResponder.cs index 64b5d70..6a3021e 100644 --- a/Modules/AutoResponder/AutoResponder.cs +++ b/Modules/AutoResponder/AutoResponder.cs @@ -45,7 +45,7 @@ internal class AutoResponder : RegexbotModule { if (def.Command == null) { await msg.Channel.SendMessageAsync(def.GetResponse()); } else { - var cmdline = def.Command.Split(new char[] { ' ' }, 2); + var cmdline = def.Command.Split([' '], 2); var ps = new ProcessStartInfo() { FileName = cmdline[0], diff --git a/Modules/EntryRole/EntryRole.cs b/Modules/EntryRole/EntryRole.cs index 7c2815f..7b12489 100644 --- a/Modules/EntryRole/EntryRole.cs +++ b/Modules/EntryRole/EntryRole.cs @@ -83,7 +83,7 @@ internal sealed class EntryRole : RegexbotModule, IDisposable { foreach (var g in DiscordClient.Guilds) { subworkers.Add(RoleApplyGuildSubWorker(g)); } - Task.WaitAll(subworkers.ToArray()); + Task.WaitAll([.. subworkers]); } } diff --git a/Modules/EntryRole/GuildData.cs b/Modules/EntryRole/GuildData.cs index b87c8ba..0000951 100644 --- a/Modules/EntryRole/GuildData.cs +++ b/Modules/EntryRole/GuildData.cs @@ -21,7 +21,7 @@ class GuildData { const int WaitTimeMax = 600; // 10 minutes - public GuildData(JObject conf) : this(conf, new Dictionary()) { } + public GuildData(JObject conf) : this(conf, []) { } public GuildData(JObject conf, Dictionary _waitingList) { WaitingList = _waitingList; diff --git a/Modules/ModCommands/ModuleConfig.cs b/Modules/ModCommands/ModuleConfig.cs index 9b7418e..bc71d4b 100644 --- a/Modules/ModCommands/ModuleConfig.cs +++ b/Modules/ModCommands/ModuleConfig.cs @@ -17,9 +17,9 @@ class ModuleConfig { Label = def[nameof(Label)]?.Value() ?? throw new ModuleLoadException($"'{nameof(Label)}' was not defined in a command definition."); var cmd = CreateCommandInstance(instance, def); - if (commands.ContainsKey(cmd.Command)) { + if (commands.TryGetValue(cmd.Command, out CommandConfig? existing)) { throw new ModuleLoadException( - $"{Label}: The command name '{cmd.Command}' is already in use by '{commands[cmd.Command].Label}'."); + $"{Label}: The command name '{cmd.Command}' is already in use by '{existing.Label}'."); } commands.Add(cmd.Command, cmd); } diff --git a/Modules/RegexModerator/ResponseExecutor.cs b/Modules/RegexModerator/ResponseExecutor.cs index 33ee347..e4500f0 100644 --- a/Modules/RegexModerator/ResponseExecutor.cs +++ b/Modules/RegexModerator/ResponseExecutor.cs @@ -33,7 +33,7 @@ class ResponseExecutor { _user = (SocketGuildUser)msg.Author; _guild = _user.Guild; - _reports = new(); + _reports = []; Log = logger; } diff --git a/Modules/VoiceRoleSync/ModuleConfig.cs b/Modules/VoiceRoleSync/ModuleConfig.cs index b959b27..7c4cb40 100644 --- a/Modules/VoiceRoleSync/ModuleConfig.cs +++ b/Modules/VoiceRoleSync/ModuleConfig.cs @@ -23,9 +23,7 @@ class ModuleConfig { } catch (FormatException) { throw new ModuleLoadException($"'{item.Name}' is not specified as a role."); } - var role = name.FindRoleIn(g); - if (role == null) throw new ModuleLoadException($"Unable to find role '{name}'."); - + var role = name.FindRoleIn(g) ?? throw new ModuleLoadException($"Unable to find role '{name}'."); var channels = Utilities.LoadStringOrStringArray(item.Value); if (channels.Count == 0) throw new ModuleLoadException($"One or more channels must be defined under '{name}'."); foreach (var id in channels) { diff --git a/RegexbotModule.cs b/RegexbotModule.cs index ca84ada..ca95e89 100644 --- a/RegexbotModule.cs +++ b/RegexbotModule.cs @@ -10,24 +10,20 @@ namespace RegexBot; /// /// Implementing classes should not rely on local variables to store runtime or state data for guilds. /// Instead, use and . +///

+/// Additionally, do not assume that is available during the constructor. ///
-public abstract class RegexbotModule { +public abstract class RegexbotModule(RegexbotClient bot) { /// /// Retrieves the bot instance. /// - public RegexbotClient Bot { get; } + public RegexbotClient Bot { get; } = bot; /// /// Retrieves the Discord client instance. /// public DiscordSocketClient DiscordClient { get => Bot.DiscordClient; } - /// - /// Called when a module is being loaded. - /// At this point, all bot services are available, but Discord is not. Do not use . - /// - public RegexbotModule(RegexbotClient bot) => Bot = bot; - /// /// Gets the name of this module. /// diff --git a/Services/CommonFunctions/CommonFunctionsService.cs b/Services/CommonFunctions/CommonFunctionsService.cs index 5b33f8a..6356c85 100644 --- a/Services/CommonFunctions/CommonFunctionsService.cs +++ b/Services/CommonFunctions/CommonFunctionsService.cs @@ -6,10 +6,9 @@ namespace RegexBot.Services.CommonFunctions { /// functions may help enforce a sense of consistency across modules when performing common actions, and may /// inform services which provide any additional features the ability to respond to those actions ahead of time. /// - internal partial class CommonFunctionsService : Service { + internal partial class CommonFunctionsService(RegexbotClient bot) : Service(bot) { // Note: Several classes within this service created by its hooks are meant to be sent to modules, // therefore those public classes are placed into the root RegexBot namespace for the developer's convenience. - public CommonFunctionsService(RegexbotClient bot) : base(bot) { } } } diff --git a/Services/EntityCache/UserCachingSubservice.cs b/Services/EntityCache/UserCachingSubservice.cs index e05360b..4f8b63f 100644 --- a/Services/EntityCache/UserCachingSubservice.cs +++ b/Services/EntityCache/UserCachingSubservice.cs @@ -86,7 +86,7 @@ class UserCachingSubservice { if (sID.HasValue) query = query.Where(c => c.UserId == (long)sID.Value); if (nameSearch != null) { - query = query.Where(c => c.Username.ToLower() == nameSearch.Value.name.ToLower()); + query = query.Where(c => c.Username.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase)); if (nameSearch.Value.disc != null) query = query.Where(c => c.Discriminator == nameSearch.Value.disc); } query = query.OrderByDescending(e => e.ULastUpdateTime); @@ -95,7 +95,7 @@ class UserCachingSubservice { } // Is search actually a ping? Extract ID. - var m = Utilities.UserMention.Match(search); + var m = Utilities.UserMentionRegex().Match(search); if (m.Success) search = m.Groups["snowflake"].Value; // Is search a number? Assume ID, proceed to query. @@ -117,8 +117,9 @@ class UserCachingSubservice { if (sID.HasValue) query = query.Where(c => c.UserId == (long)sID.Value); if (nameSearch != null) { - query = query.Where(c => (c.Nickname != null && c.Nickname.ToLower() == nameSearch.Value.name.ToLower()) || - c.User.Username.ToLower() == nameSearch.Value.name.ToLower()); + query = query.Where(c => (c.Nickname != null + && c.Nickname.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase)) + || c.User.Username.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase)); if (nameSearch.Value.disc != null) query = query.Where(c => c.User.Discriminator == nameSearch.Value.disc); } query = query.OrderByDescending(e => e.GULastUpdateTime); @@ -127,7 +128,7 @@ class UserCachingSubservice { } // Is search actually a ping? Extract ID. - var m = Utilities.UserMention.Match(search); + var m = Utilities.UserMentionRegex().Match(search); if (m.Success) search = m.Groups["snowflake"].Value; // Is search a number? Assume ID, proceed to query. @@ -144,7 +145,7 @@ class UserCachingSubservice { private static (string, string?) SplitNameAndDiscriminator(string input) { string name; string? disc = null; - var split = Utilities.DiscriminatorSearch.Match(input); + var split = Utilities.DiscriminatorSearchRegex().Match(input); if (split.Success) { name = split.Groups[1].Value; disc = split.Groups[2].Value; diff --git a/Services/Logging/LoggingService.cs b/Services/Logging/LoggingService.cs index d3744e0..f9fbdc4 100644 --- a/Services/Logging/LoggingService.cs +++ b/Services/Logging/LoggingService.cs @@ -52,7 +52,7 @@ class LoggingService : Service { var now = DateTimeOffset.Now; var output = new StringBuilder(); var prefix = $"[{now:s}] [{source}] "; - foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) { + foreach (var line in message.Split(["\r\n", "\n"], StringSplitOptions.None)) { output.Append(prefix).AppendLine(line); } var outstr = output.ToString(); diff --git a/Services/ModuleState/ModuleStateService.cs b/Services/ModuleState/ModuleStateService.cs index 93623e7..ceed686 100644 --- a/Services/ModuleState/ModuleStateService.cs +++ b/Services/ModuleState/ModuleStateService.cs @@ -6,12 +6,12 @@ namespace RegexBot.Services.ModuleState; /// class ModuleStateService : Service { private readonly Dictionary _moderators; - private readonly Dictionary> _stateData; + private readonly Dictionary> _guildStates; private readonly JObject _serverConfs; public ModuleStateService(RegexbotClient bot, JObject servers) : base(bot) { - _moderators = new(); - _stateData = new(); + _moderators = []; + _guildStates = []; _serverConfs = servers; bot.DiscordClient.GuildAvailable += RefreshGuildState; @@ -25,17 +25,20 @@ class ModuleStateService : Service { } private Task RemoveGuildData(SocketGuild arg) { - _stateData.Remove(arg.Id); + _guildStates.Remove(arg.Id); _moderators.Remove(arg.Id); return Task.CompletedTask; } // Hooked public T? DoGetStateObj(ulong guildId, Type t) { - if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) { - // Leave handling of potential InvalidCastException to caller. - return (T?)_stateData[guildId][t]; + if (_guildStates.TryGetValue(guildId, out var guildConfs)) { + if (guildConfs.TryGetValue(t, out var moduleConf)) { + // Leave handling of potential InvalidCastException to caller. + return (T?)moduleConf; + } } + return default; } @@ -71,7 +74,7 @@ class ModuleStateService : Service { } } _moderators[guild.Id] = mods; - _stateData[guild.Id] = newStates; + _guildStates[guild.Id] = newStates; return true; } } diff --git a/Services/Service.cs b/Services/Service.cs index cdf0b2d..a504286 100644 --- a/Services/Service.cs +++ b/Services/Service.cs @@ -6,13 +6,11 @@ /// Services provide core and shared functionality for this program. Modules are expected to call into services /// directly or indirectly in order to access bot features. /// -internal abstract class Service { - public RegexbotClient BotClient { get; } +internal abstract class Service(RegexbotClient bot) { + public RegexbotClient BotClient { get; } = bot; public string Name => GetType().Name; - public Service(RegexbotClient bot) => BotClient = bot; - /// /// Emits a log message. /// From 986d9e4ff3b27d1b423bc7d3b85ed57ee6c67bce Mon Sep 17 00:00:00 2001 From: Noi Date: Sat, 8 Jun 2024 21:08:03 -0700 Subject: [PATCH 3/3] Implement suggestions; minor command refactoring --- Modules/ModCommands/Commands/BanKick.cs | 6 ++--- Modules/ModCommands/Commands/CommandConfig.cs | 26 ++++++++++++------- Modules/ModCommands/Commands/ConfReload.cs | 5 +--- Modules/ModCommands/Commands/NoteWarn.cs | 10 +++---- .../ModCommands/Commands/RoleManipulation.cs | 8 +++--- Modules/ModCommands/Commands/Say.cs | 4 +-- Modules/ModCommands/Commands/ShowModLogs.cs | 7 +++-- Modules/ModCommands/Commands/Timeout.cs | 2 +- Modules/ModCommands/Commands/Unban.cs | 2 +- Modules/ModCommands/Commands/Untimeout.cs | 2 +- Modules/PendingOutRole/ModuleConfig.cs | 1 - 11 files changed, 33 insertions(+), 40 deletions(-) diff --git a/Modules/ModCommands/Commands/BanKick.cs b/Modules/ModCommands/Commands/BanKick.cs index 2f44bef..0732f8b 100644 --- a/Modules/ModCommands/Commands/BanKick.cs +++ b/Modules/ModCommands/Commands/BanKick.cs @@ -22,9 +22,7 @@ class Ban : BanKick { } } -class Kick : BanKick { - public Kick(ModCommands module, JObject config) : base(module, config, false) { } - +class Kick(ModCommands module, JObject config) : BanKick(module, config, false) { protected override async Task ContinueInvoke(SocketGuild g, SocketMessage msg, string? reason, ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser) { // Kick: Unlike ban, must find the guild user in order to proceed @@ -76,7 +74,7 @@ abstract class BanKick : CommandConfig { // Usage: (command) (user) (reason) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); string targetstr; string? reason; if (line.Length < 2) { diff --git a/Modules/ModCommands/Commands/CommandConfig.cs b/Modules/ModCommands/Commands/CommandConfig.cs index bda304c..82629c3 100644 --- a/Modules/ModCommands/Commands/CommandConfig.cs +++ b/Modules/ModCommands/Commands/CommandConfig.cs @@ -3,16 +3,10 @@ using System.Diagnostics; namespace RegexBot.Modules.ModCommands.Commands; [DebuggerDisplay("Command definition '{Label}'")] -abstract class CommandConfig { - public string Label { get; } - public string Command { get; } - protected ModCommands Module { get; } - - protected CommandConfig(ModCommands module, JObject config) { - Module = module; - Label = config[nameof(Label)]!.Value()!; - Command = config[nameof(Command)]!.Value()!; - } +abstract class CommandConfig(ModCommands module, JObject config) { + public string Label { get; } = config[nameof(Label)]!.Value()!; + public string Command { get; } = config[nameof(Command)]!.Value()!; + protected ModCommands Module { get; } = module; public abstract Task Invoke(SocketGuild g, SocketMessage msg); @@ -20,6 +14,7 @@ abstract class CommandConfig { protected const string TargetNotFound = ":x: **Unable to find the given user.**"; protected abstract string DefaultUsageMsg { get; } + /// /// Sends out the default usage message () within an embed. /// An optional message can be included, for uses such as notifying users of incorrect usage. @@ -36,4 +31,15 @@ abstract class CommandConfig { }; await target.SendMessageAsync(message ?? "", embed: usageEmbed.Build()); } + + internal static readonly char[] separator = [' ']; + /// + /// For the given message's content, assumes its message is a command and returns its parameters + /// as an array of substrings. + /// + /// The incoming message to process. + /// The number of parameters to expect. + /// A string array with 0 to maxParams - 1 elements. + protected static string[] SplitToParams(SocketMessage msg, int maxParams) + => msg.Content.Split(separator, maxParams, StringSplitOptions.RemoveEmptyEntries); } diff --git a/Modules/ModCommands/Commands/ConfReload.cs b/Modules/ModCommands/Commands/ConfReload.cs index fadbd06..5e76927 100644 --- a/Modules/ModCommands/Commands/ConfReload.cs +++ b/Modules/ModCommands/Commands/ConfReload.cs @@ -1,10 +1,7 @@ namespace RegexBot.Modules.ModCommands.Commands; -class ConfReload : CommandConfig { +class ConfReload(ModCommands module, JObject config) : CommandConfig(module, config) { protected override string DefaultUsageMsg => null!; - // No configuration. - public ConfReload(ModCommands module, JObject config) : base(module, config) { } - // Usage: (command) public override Task Invoke(SocketGuild g, SocketMessage msg) { throw new NotImplementedException(); diff --git a/Modules/ModCommands/Commands/NoteWarn.cs b/Modules/ModCommands/Commands/NoteWarn.cs index e809a94..8126c90 100644 --- a/Modules/ModCommands/Commands/NoteWarn.cs +++ b/Modules/ModCommands/Commands/NoteWarn.cs @@ -2,9 +2,7 @@ using RegexBot.Common; namespace RegexBot.Modules.ModCommands.Commands; // Note and Warn commands are highly similar in implementnation, and thus are handled in a single class. -class Note : NoteWarn { - public Note(ModCommands module, JObject config) : base(module, config) { } - +class Note(ModCommands module, JObject config) : NoteWarn(module, config) { protected override string DefaultUsageMsg => string.Format(_usageHeader, Command) + "Appends a note to the moderation log for the given user."; protected override async Task ContinueInvoke(SocketGuild g, SocketMessage msg, string logMessage, SocketUser targetUser) { @@ -13,9 +11,7 @@ class Note : NoteWarn { } } -class Warn : NoteWarn { - public Warn(ModCommands module, JObject config) : base(module, config) { } - +class Warn(ModCommands module, JObject config) : NoteWarn(module, config) { protected override string DefaultUsageMsg => string.Format(_usageHeader, Command) + "Issues a warning to the given user, logging the instance to this bot's moderation log " + "and notifying the offending user over DM of the issued warning."; @@ -45,7 +41,7 @@ abstract class NoteWarn : CommandConfig { // Usage: (command) (user) (message) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); if (line.Length != 3) { await SendUsageMessageAsync(msg.Channel, ":x: Not all required parameters were specified."); return; diff --git a/Modules/ModCommands/Commands/RoleManipulation.cs b/Modules/ModCommands/Commands/RoleManipulation.cs index 99e0483..0dd9bec 100644 --- a/Modules/ModCommands/Commands/RoleManipulation.cs +++ b/Modules/ModCommands/Commands/RoleManipulation.cs @@ -1,17 +1,15 @@ using RegexBot.Common; namespace RegexBot.Modules.ModCommands.Commands; -class RoleAdd : RoleManipulation { +class RoleAdd(ModCommands module, JObject config) : RoleManipulation(module, config) { protected override (string, string) String1 => ("Adds", "to"); protected override string String2 => "set"; - public RoleAdd(ModCommands module, JObject config) : base(module, config) { } protected override async Task ContinueInvoke(SocketGuildUser target, SocketRole role) => await target.AddRoleAsync(role); } -class RoleDel : RoleManipulation { +class RoleDel(ModCommands module, JObject config) : RoleManipulation(module, config) { protected override (string, string) String1 => ("Removes", "from"); protected override string String2 => "unset"; - public RoleDel(ModCommands module, JObject config) : base(module, config) { } protected override async Task ContinueInvoke(SocketGuildUser target, SocketRole role) => await target.RemoveRoleAsync(role); } @@ -46,7 +44,7 @@ abstract class RoleManipulation : CommandConfig { public override async Task Invoke(SocketGuild g, SocketMessage msg) { // TODO reason in further parameters? - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); string targetstr; if (line.Length < 2) { await SendUsageMessageAsync(msg.Channel, null); diff --git a/Modules/ModCommands/Commands/Say.cs b/Modules/ModCommands/Commands/Say.cs index f35de3d..7be632b 100644 --- a/Modules/ModCommands/Commands/Say.cs +++ b/Modules/ModCommands/Commands/Say.cs @@ -13,7 +13,7 @@ class Say : CommandConfig { } public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); if (line.Length <= 1) { await SendUsageMessageAsync(msg.Channel, ":x: You must specify a channel."); return; @@ -23,7 +23,7 @@ class Say : CommandConfig { return; } - var getCh = Utilities.ChannelMention.Match(line[1]); + var getCh = Utilities.ChannelMentionRegex().Match(line[1]); if (!getCh.Success) { await SendUsageMessageAsync(msg.Channel, ":x: Unable to find given channel."); return; diff --git a/Modules/ModCommands/Commands/ShowModLogs.cs b/Modules/ModCommands/Commands/ShowModLogs.cs index dab9c6f..6ef44fd 100644 --- a/Modules/ModCommands/Commands/ShowModLogs.cs +++ b/Modules/ModCommands/Commands/ShowModLogs.cs @@ -20,7 +20,7 @@ class ShowModLogs : CommandConfig { // Usage: (command) (query) [page] public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); if (line.Length < 2) { await SendUsageMessageAsync(msg.Channel, null); return; @@ -47,13 +47,12 @@ class ShowModLogs : CommandConfig { .Where(l => l.GuildId == query.GuildId && l.UserId == query.UserId) .Count(); totalPages = (int)Math.Ceiling((double)totalEntries / LogEntriesPerMessage); - results = db.ModLogs + results = [.. db.ModLogs .Where(l => l.GuildId == query.GuildId && l.UserId == query.UserId) .OrderByDescending(l => l.LogId) .Skip((pagenum - 1) * LogEntriesPerMessage) .Take(LogEntriesPerMessage) - .AsNoTracking() - .ToList(); + .AsNoTracking()]; } var resultList = new EmbedBuilder() { diff --git a/Modules/ModCommands/Commands/Timeout.cs b/Modules/ModCommands/Commands/Timeout.cs index a106a91..964284e 100644 --- a/Modules/ModCommands/Commands/Timeout.cs +++ b/Modules/ModCommands/Commands/Timeout.cs @@ -26,7 +26,7 @@ class Timeout : CommandConfig { // Usage: (command) (user) (duration) (reason) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 4, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 4); string targetstr; string? reason; if (line.Length < 3) { diff --git a/Modules/ModCommands/Commands/Unban.cs b/Modules/ModCommands/Commands/Unban.cs index 9013ef4..f2be40b 100644 --- a/Modules/ModCommands/Commands/Unban.cs +++ b/Modules/ModCommands/Commands/Unban.cs @@ -16,7 +16,7 @@ class Unban : CommandConfig { // Usage: (command) (user query) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); string targetstr; if (line.Length < 2) { await SendUsageMessageAsync(msg.Channel, null); diff --git a/Modules/ModCommands/Commands/Untimeout.cs b/Modules/ModCommands/Commands/Untimeout.cs index 87903a6..ae208ec 100644 --- a/Modules/ModCommands/Commands/Untimeout.cs +++ b/Modules/ModCommands/Commands/Untimeout.cs @@ -16,7 +16,7 @@ class Untimeout : CommandConfig { // Usage: (command) (user query) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + var line = SplitToParams(msg, 3); string targetstr; if (line.Length < 2) { await SendUsageMessageAsync(msg.Channel, null); diff --git a/Modules/PendingOutRole/ModuleConfig.cs b/Modules/PendingOutRole/ModuleConfig.cs index 90c601a..8c555b6 100644 --- a/Modules/PendingOutRole/ModuleConfig.cs +++ b/Modules/PendingOutRole/ModuleConfig.cs @@ -12,6 +12,5 @@ class ModuleConfig { } catch (FormatException) { throw new ModuleLoadException("Name specified in configuration is not a role."); } - } }