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/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/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/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/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.");
}
-
}
}
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/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
-
-
-
-
+
+
+
+
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.
///