diff --git a/.editorconfig b/.editorconfig index cde0129..0b8c1b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -75,59 +75,59 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = false:silent # Expression-bodied members -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = true -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = true -csharp_style_expression_bodied_methods = true -csharp_style_expression_bodied_operators = true -csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_pattern_matching = true:suggestion -csharp_style_prefer_switch_expression = true +csharp_style_prefer_switch_expression = true:suggestion # Null-checking preferences -csharp_style_conditional_delegate_call = true +csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences -csharp_prefer_static_local_function = true +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 -csharp_prefer_simple_using_statement = true -csharp_style_namespace_declarations = file_scoped +csharp_prefer_braces = when_multiline:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion # Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion csharp_style_pattern_local_over_anonymous_function = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences -csharp_using_directive_placement = outside_namespace +csharp_using_directive_placement = outside_namespace:silent # New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false -csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent #### C# Formatting Rules #### @@ -217,3 +217,42 @@ dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case +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 diff --git a/.gitignore b/.gitignore index 65261fc..bd521b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ [Oo]bj/ .vs/ *.user +output/ \ No newline at end of file diff --git a/RegexBot-Modules/RateLimit.cs b/RegexBot-Modules/RateLimit.cs index 373a2eb..8657a1b 100644 --- a/RegexBot-Modules/RateLimit.cs +++ b/RegexBot-Modules/RateLimit.cs @@ -3,15 +3,16 @@ /// /// Helper class for managing rate limit data. /// More accurately, this class holds entries, not allowing the same entry to be held more than once until a specified -/// amount of time has paspsed since the entry was originally tracked; useful for a rate limit system. +/// amount of time has passed since the entry was originally tracked; useful for a rate limit system. /// -class RateLimit { +class RateLimit where T : notnull { public const ushort DefaultTimeout = 20; // Skeeter's a cool guy and you can't convince me otherwise. + /// + /// Time until an entry within this instance expires, in seconds. + /// public uint Timeout { get; } -#pragma warning disable CS8714 private Dictionary Entries { get; } = new Dictionary(); -#pragma warning restore CS8714 public RateLimit() : this(DefaultTimeout) { } public RateLimit(uint timeout) => Timeout = timeout; diff --git a/RegexBot-Modules/RegexBot-Modules.csproj b/RegexBot-Modules/RegexBot-Modules.csproj index a3b6ab8..50200f0 100644 --- a/RegexBot-Modules/RegexBot-Modules.csproj +++ b/RegexBot-Modules/RegexBot-Modules.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -7,6 +7,9 @@ enable NoiTheCat A set of standard modules for use with RegexBot. + $(SolutionDir)\output + true + embedded diff --git a/RegexBot/Common/FilterList.cs b/RegexBot/Common/FilterList.cs index 6c7aca2..87135d0 100644 --- a/RegexBot/Common/FilterList.cs +++ b/RegexBot/Common/FilterList.cs @@ -25,13 +25,9 @@ public class FilterList { /// /// Thrown if there is a problem with input. The exception message specifies the reason. /// - public FilterList( - JObject config, - string whitelistKey = "whitelist", - string blacklistKey = "blacklist", - string exemptKey = "exempt") { - if ((whitelistKey != null && config[whitelistKey] != null) && - (blacklistKey != null && config[blacklistKey] != null)) { + public FilterList(JObject config, string whitelistKey = "whitelist", string blacklistKey = "blacklist", string exemptKey = "exempt") { + if (whitelistKey != null && config[whitelistKey] != null && + blacklistKey != null && config[blacklistKey] != null) { throw new FormatException($"Cannot have both '{whitelistKey}' and '{blacklistKey}' defined at once."); } @@ -83,12 +79,12 @@ public class FilterList { public bool IsFiltered(SocketMessage msg, bool keepId) { if (Mode == FilterMode.None) return false; - bool inFilter = FilteredList.IsListMatch(msg, keepId); + var isInFilter = FilteredList.IsListMatch(msg, keepId); if (Mode == FilterMode.Whitelist) { - if (!inFilter) return true; + if (!isInFilter) return true; return FilterExemptions.IsListMatch(msg, keepId); } else if (Mode == FilterMode.Blacklist) { - if (!inFilter) return false; + if (!isInFilter) return false; return !FilterExemptions.IsListMatch(msg, keepId); } diff --git a/RegexBot/Data/BotDatabaseContext.cs b/RegexBot/Data/BotDatabaseContext.cs index 347bbfe..40bb2c5 100644 --- a/RegexBot/Data/BotDatabaseContext.cs +++ b/RegexBot/Data/BotDatabaseContext.cs @@ -4,7 +4,7 @@ namespace RegexBot.Data; public class BotDatabaseContext : DbContext { private static string? _npgsqlConnectionString; - internal static string NpgsqlConnectionString { + internal static string PostgresConnectionString { #if DEBUG get { if (_npgsqlConnectionString != null) return _npgsqlConnectionString; @@ -24,7 +24,7 @@ public class BotDatabaseContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(NpgsqlConnectionString) + .UseNpgsql(PostgresConnectionString) .UseSnakeCaseNamingConvention(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/RegexBot/InstanceConfig.cs b/RegexBot/InstanceConfig.cs index 051f904..e5a4f70 100644 --- a/RegexBot/InstanceConfig.cs +++ b/RegexBot/InstanceConfig.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using RegexBot.Data; using System.Reflection; namespace RegexBot; @@ -19,11 +20,6 @@ class InstanceConfig { /// internal string[] Assemblies { get; } - /// - /// Connection string for accessing the PostgreSQL database. - /// - internal string PostgresConnString { get; } - /// /// Webhook URL for bot log reporting. /// @@ -37,7 +33,7 @@ class InstanceConfig { internal InstanceConfig(string[] cmdline) { var opts = Options.ParseOptions(cmdline); - string path = opts.ConfigFile; + var path = opts.ConfigFile; if (path == null) { // default: config.json in working directory path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + "." + Path.DirectorySeparatorChar + "config.json"; @@ -60,9 +56,10 @@ class InstanceConfig { if (string.IsNullOrEmpty(BotToken)) throw new Exception($"'{nameof(BotToken)}' is not properly specified in configuration."); - PostgresConnString = conf[nameof(PostgresConnString)]?.Value()!; - if (string.IsNullOrEmpty(PostgresConnString)) - throw new Exception($"'{nameof(PostgresConnString)}' is not properly specified in configuration."); + var pginput = conf[nameof(BotDatabaseContext.PostgresConnectionString)]?.Value()!; + if (string.IsNullOrEmpty(pginput)) + throw new Exception($"'{nameof(BotDatabaseContext.PostgresConnectionString)}' is not properly specified in configuration."); + BotDatabaseContext.PostgresConnectionString = pginput; InstanceLogTarget = conf[nameof(InstanceLogTarget)]?.Value()!; if (string.IsNullOrEmpty(InstanceLogTarget)) diff --git a/RegexBot/ModuleLoader.cs b/RegexBot/ModuleLoader.cs index 43bda08..03e59ea 100644 --- a/RegexBot/ModuleLoader.cs +++ b/RegexBot/ModuleLoader.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Reflection; +using System.Text; namespace RegexBot; @@ -42,12 +43,14 @@ static class ModuleLoader { select type; k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}"); + var newreport = new StringBuilder("---> Found module(s):"); var newmods = new List(); foreach (var t in eligibleTypes) { var mod = Activator.CreateInstance(t, k)!; - k._svcLogging.DoLog(false, nameof(ModuleLoader), $"---> Loading module {t.FullName}"); + newreport.Append($" {t.Name}"); newmods.Add((RegexbotModule)mod); } + k._svcLogging.DoLog(false, nameof(ModuleLoader), newreport.ToString()); return newmods; } } diff --git a/RegexBot/RegexBot.csproj b/RegexBot/RegexBot.csproj index 1da133a..a4a35bc 100644 --- a/RegexBot/RegexBot.csproj +++ b/RegexBot/RegexBot.csproj @@ -8,11 +8,8 @@ 0.0.1 enable enable - - - - none - false + True + $(SolutionDir)\output @@ -24,13 +21,15 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/RegexBot/RegexbotClient.cs b/RegexBot/RegexbotClient.cs index fc66b1f..b5c1a76 100644 --- a/RegexBot/RegexbotClient.cs +++ b/RegexBot/RegexbotClient.cs @@ -32,11 +32,10 @@ public partial class RegexbotClient { _svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this); _svcEntityCache = new Services.EntityCache.EntityCacheService(this); + var ver = Assembly.GetExecutingAssembly().GetName().Version!; + _svcLogging.DoLog(true, nameof(RegexBot), $"{nameof(RegexBot)} v{ver:3} - https://github.com/NoiTheCat/RegexBot"); + // Load externally defined functionality Modules = ModuleLoader.Load(Config, this); - - // Everything's ready to go. Print the welcome message here. - var ver = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); - _svcLogging.DoLog(true, nameof(RegexBot), $"{nameof(RegexBot)} v{ver} - https://github.com/NoiTheCat/RegexBot"); } } diff --git a/RegexBot/Services/CommonFunctions/CommonFunctionsService.cs b/RegexBot/Services/CommonFunctions/CommonFunctionsService.cs index 1f5c5a5..18d48cc 100644 --- a/RegexBot/Services/CommonFunctions/CommonFunctionsService.cs +++ b/RegexBot/Services/CommonFunctions/CommonFunctionsService.cs @@ -1,6 +1,5 @@ using Discord.Net; using Discord.WebSocket; -using static RegexBot.RegexbotClient; namespace RegexBot.Services.CommonFunctions; @@ -16,14 +15,15 @@ internal class CommonFunctionsService : Service { public CommonFunctionsService(RegexbotClient bot) : base(bot) { } #region Guild member removal - /// - /// Common processing for kicks and bans. Called by DoKickAsync and DoBanAsync. - /// - /// The reason to insert into the Audit Log. + // Hooked (indirectly) [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] - internal async Task BanOrKickAsync( - RemovalType t, SocketGuild guild, string source, ulong target, int banPurgeDays, - string logReason, bool sendDmToTarget) { + internal async Task BanOrKickAsync(RemovalType t, + SocketGuild guild, + string source, + ulong target, + int banPurgeDays, + string? logReason, + bool sendDmToTarget) { if (t == RemovalType.None) throw new ArgumentException("Removal type must be 'ban' or 'kick'."); if (string.IsNullOrWhiteSpace(logReason)) logReason = "Reason not specified."; var dmSuccess = true; @@ -42,9 +42,9 @@ internal class CommonFunctionsService : Service { // Perform the action try { - if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays); + if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays, logReason); else await utarget!.KickAsync(logReason); - // TODO !! Insert ban reason! For kick also: Figure out a way to specify invoker. + // TODO for kick: Figure out a way to specify invoker. } catch (HttpException ex) { return new BanKickResult(ex, dmSuccess, false, t, target); } diff --git a/RegexBot/Services/CommonFunctions/Hooks.cs b/RegexBot/Services/CommonFunctions/Hooks.cs index b531c58..277f241 100644 --- a/RegexBot/Services/CommonFunctions/Hooks.cs +++ b/RegexBot/Services/CommonFunctions/Hooks.cs @@ -5,8 +5,6 @@ namespace RegexBot; partial class RegexbotClient { private readonly CommonFunctionsService _svcCommonFunctions; - public enum RemovalType { None, Ban, Kick } - /// /// Attempts to ban the given user from the specified guild. It is greatly preferred to call this method /// instead of manually executing the equivalent method found in Discord.Net. It notifies other services @@ -19,8 +17,12 @@ partial class RegexbotClient { /// Number of days of prior post history to delete on ban. Must be between 0-7. /// Reason for the action. Sent to the Audit Log and user (if specified). /// Specify whether to send a direct message to the target user informing them of the action. - public Task BanAsync(SocketGuild guild, string source, ulong targetUser, - int purgeDays, string reason, bool sendDMToTarget) + public Task BanAsync(SocketGuild guild, + string source, + ulong targetUser, + int purgeDays, + string? reason, + bool sendDMToTarget) => _svcCommonFunctions.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, sendDMToTarget); /// @@ -28,8 +30,12 @@ partial class RegexbotClient { /// EntityCache lookup to determine the target. /// /// The EntityCache search string. - public async Task BanAsync(SocketGuild guild, string source, string targetSearch, - int purgeDays, string reason, bool sendDMToTarget) { + public async Task BanAsync(SocketGuild guild, + string source, + string targetSearch, + int purgeDays, + string? reason, + bool sendDMToTarget) { var result = EcQueryGuildUser(guild.Id, targetSearch); if (result == null) return new BanKickResult(null, false, true, RemovalType.Ban, 0); return await BanAsync(guild, source, (ulong)result.UserId, purgeDays, reason, sendDMToTarget); @@ -52,15 +58,23 @@ partial class RegexbotClient { /// Specify whether to send a direct message to the target user informing them of the action /// (that is, a ban/kick message). /// - public Task KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, bool sendDMToTarget) - => _svcCommonFunctions.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget); + public Task KickAsync(SocketGuild guild, + string source, + ulong targetUser, + string? reason, + bool sendDMToTarget) + => _svcCommonFunctions.BanOrKickAsync(RemovalType.Kick, guild, source, targetUser, default, reason, sendDMToTarget); /// /// Similar to , but making use of an /// EntityCache lookup to determine the target. /// /// The EntityCache search string. - public async Task KickAsync(SocketGuild guild, string source, string targetSearch, string reason, bool sendDMToTarget) { + public async Task KickAsync(SocketGuild guild, + string source, + string targetSearch, + string? reason, + bool sendDMToTarget) { var result = EcQueryGuildUser(guild.Id, targetSearch); if (result == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0); return await KickAsync(guild, source, (ulong)result.UserId, reason, sendDMToTarget); diff --git a/RegexBot/Services/CommonFunctions/RemovalType.cs b/RegexBot/Services/CommonFunctions/RemovalType.cs new file mode 100644 index 0000000..2a2ee86 --- /dev/null +++ b/RegexBot/Services/CommonFunctions/RemovalType.cs @@ -0,0 +1,20 @@ +// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too, +// thus it is placed within the root namespace. +namespace RegexBot; +/// +/// Specifies possible outcomes for the removal of a user from a guild. +/// +public enum RemovalType { + /// + /// Default value. Not used in any actual circumstances. + /// + None, + /// + /// Specifies that the type of removal includes placing the user on the guild's ban list. + /// + Ban, + /// + /// Specifies that the user is removed from the server via kick. + /// + Kick +}