Abandoning old plans, project will continue as RegexBot 3.0

This commit is contained in:
Noikoio 2021-08-25 20:18:45 -07:00 committed by Noi
parent 6b83735d6b
commit a7de9132ed
44 changed files with 608 additions and 339 deletions

219
.editorconfig Normal file
View file

@ -0,0 +1,219 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = false
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
# 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
# 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_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
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
# 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_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
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# 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
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = false
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = false
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = flush_left
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
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

View file

@ -1,37 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kerobot", "Kerobot\Kerobot.csproj", "{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules-PublicInstance", "Modules-PublicInstance\Modules-PublicInstance.csproj", "{E06B76BE-E6F5-4803-BA02-DA8AF2A9705E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modules-SelfHosted", "Modules-SelfHosted\Modules-SelfHosted.csproj", "{84390AD6-6906-4BD9-A948-578C21037D6C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Release|Any CPU.Build.0 = Release|Any CPU
{E06B76BE-E6F5-4803-BA02-DA8AF2A9705E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E06B76BE-E6F5-4803-BA02-DA8AF2A9705E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E06B76BE-E6F5-4803-BA02-DA8AF2A9705E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E06B76BE-E6F5-4803-BA02-DA8AF2A9705E}.Release|Any CPU.Build.0 = Release|Any CPU
{84390AD6-6906-4BD9-A948-578C21037D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84390AD6-6906-4BD9-A948-578C21037D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84390AD6-6906-4BD9-A948-578C21037D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84390AD6-6906-4BD9-A948-578C21037D6C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C823EC9C-CF20-4437-8B99-72158A6DD113}
EndGlobalSection
EndGlobal

View file

@ -1,72 +0,0 @@
using Discord.Net;
namespace Kerobot
{
// Instances of this are created by CommonFunctionService and by ModuleBase on behalf of CommonFunctionService,
// and are meant to be sent to modules. This class has therefore been put within the Kerobot namespace.
/// <summary>
/// Contains information on various success/failure outcomes for a ban or kick operation.
/// </summary>
public class BanKickResult
{
private readonly bool _userNotFound;
internal BanKickResult(HttpException error, bool notificationSuccess, bool errorNotFound)
{
OperationError = error;
MessageSendSuccess = notificationSuccess;
_userNotFound = errorNotFound;
}
/// <summary>
/// Gets a value indicating whether the kick or ban succeeded.
/// </summary>
public bool OperationSuccess {
get {
if (ErrorNotFound) return false;
if (OperationError == null) return false;
return true;
}
}
/// <summary>
/// The exception thrown, if any, when attempting to kick or ban the target.
/// </summary>
public HttpException OperationError { get; }
/// <summary>
/// Indicates if the operation failed due to being unable to find the user.
/// </summary>
public bool ErrorNotFound
{
get
{
if (_userNotFound) return true; // TODO I don't like this.
if (OperationSuccess) return false;
return OperationError.HttpCode == System.Net.HttpStatusCode.NotFound;
}
}
/// <summary>
/// Indicates if the operation failed due to a permissions issue.
/// </summary>
public bool ErrorForbidden
{
get
{
if (OperationSuccess) return false;
return OperationError.HttpCode == System.Net.HttpStatusCode.Forbidden;
}
}
/// <summary>
/// Gets a value indicating whether the user was able to receive the ban or kick message.
/// </summary>
/// <value>
/// <see langword="false"/> if an error was encountered when attempting to send the target a DM. Will always
/// return <see langword="true"/> otherwise, including cases in which no message was sent.
/// </value>
public bool MessageSendSuccess { get; }
}
}

View file

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Authors>Noiiko</Authors>
<Product>Kerobot</Product>
<Version>0.0.1</Version>
<Description>Essential functions for Kerobot which are available in the public bot instance.</Description>
<RootNamespace>Kerobot.Modules</RootNamespace>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy /Y &quot;$(TargetDir)$(ProjectName).*&quot; &quot;$(SolutionDir)Kerobot\bin\$(ConfigurationName)\netcoreapp2.0\&quot;" />
</Target>
<ItemGroup>
<ProjectReference Include="..\Kerobot\Kerobot.csproj" />
</ItemGroup>
</Project>

View file

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Authors>Noiiko</Authors>
<Product>Kerobot</Product>
<Version>0.0.1</Version>
<Description>Kerobot modules with more specific purposes that may not work well in a public instance, but are manageable in self-hosted bot instances.</Description>
<RootNamespace>Kerobot.Modules</RootNamespace>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy /Y &quot;$(TargetDir)$(ProjectName).*&quot; &quot;$(SolutionDir)Kerobot\bin\$(ConfigurationName)\netcoreapp2.0\&quot;" />
</Target>
<ItemGroup>
<ProjectReference Include="..\Kerobot\Kerobot.csproj" />
</ItemGroup>
</Project>

View file

@ -1,9 +1,14 @@
# Kerobot
**This project is a major work in progress and is unusable in its current state. Not all features may be present.**
# RegexBot
**This branch contains code that is still a major work in progress, and is unusable in its current state. See the master branch for the current working version.**
Kerobot is a Discord moderation bot that takes some inspiration from Reddit's Automoderator. It provides a number of features to assist in watching over a busy server. Its configuration allows for a very high level of flexibility, ensuring that the bot behaves in accordance to the exact needs of your server.
RegexBot is a self-hosted Discord moderation bot that takes some inspiration from Reddit's Automoderator. It provides a number of features to assist in watching over the tedious details in a busy server with no hidden details, arbitrary restrictions, or unmodifiable behavior. Its configuration allows for a very high level of flexibility, ensuring that the bot behaves in accordance to the exact needs of your server.
### Feature overview
### Feature overview for 3.0:
* Modular structure allows for extra features to be written, further enhancing the bot's customizability wherever it may be deployed.
* Versatile JSON-based configuration, support for separate servers.
* High detail logging and record-keeping prevents gaps in moderation that might occur with large public bots.
### Feature overview for RegexBotModule 3.0:
* Create rules based on regular expression patterns
* Follow up with custom responses ranging from sending a DM to disciplinary action
* Create pattern-based triggers to provide information and fun to your users

View file

@ -3,17 +3,17 @@ using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Kerobot.Modules.AutoResponder
namespace RegexBot.Modules.AutoResponder
{
/// <summary>
/// Provides the capability to define text responses to pattern-based triggers for fun or informational
/// purposes. Although in essence similar to <see cref="RegexModerator.RegexModerator"/>, it is a better
/// fit for non-moderation use cases and has specific features suitable to that end.
/// </summary>
[KerobotModule]
[RegexbotModule]
class AutoResponder : ModuleBase
{
public AutoResponder(Kerobot kb) : base(kb)
public AutoResponder(RegexbotClient bot) : base(bot)
{
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
}

View file

@ -1,11 +1,11 @@
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Kerobot.Modules.AutoResponder
namespace RegexBot.Modules.AutoResponder
{
/// <summary>
/// Representation of a single <see cref="AutoResponder"/> configuration definition.

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Kerobot.Modules.AutoResponder
namespace RegexBot.Modules.AutoResponder
{
/// <summary>
/// Helper class for managing rate limit data.

View file

@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Kerobot.Modules.AutoScriptResponder
namespace RegexBot.Modules.AutoScriptResponder
{
/// <summary>
/// Meant to be highly identical to AutoResponder, save for its differentiating feature.
@ -12,10 +12,10 @@ namespace Kerobot.Modules.AutoScriptResponder
/// the other whenever they occur.
/// The feature in question: It executes external scripts and replies with their output.
/// </summary>
[KerobotModule]
[RegexbotModule]
class AutoScriptResponder : ModuleBase
{
public AutoScriptResponder(Kerobot kb) : base(kb)
public AutoScriptResponder(RegexbotClient bot) : base(bot)
{
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
}

View file

@ -1,11 +1,11 @@
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Kerobot.Modules.AutoScriptResponder
namespace RegexBot.Modules.AutoScriptResponder
{
/// <summary>
/// Representation of a single <see cref="AutoResponder"/> configuration definition.

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Kerobot.Modules.AutoScriptResponder
namespace RegexBot.Modules.AutoScriptResponder
{
/// <summary>
/// Helper class for managing rate limit data.

View file

@ -7,19 +7,19 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Kerobot.Modules.EntryRole
namespace RegexBot.Modules.EntryRole
{
/// <summary>
/// Automatically sets a role onto users entering the guild.
/// </summary>
// TODO add persistent role support, make it an option
[KerobotModule]
[RegexbotModule]
public class EntryRole : ModuleBase
{
readonly Task _workerTask;
readonly CancellationTokenSource _workerTaskToken; // TODO make use of this when possible
public EntryRole(Kerobot kb) : base(kb)
public EntryRole(RegexbotClient bot) : base(bot)
{
DiscordClient.UserJoined += DiscordClient_UserJoined;
DiscordClient.UserLeft += DiscordClient_UserLeft;

View file

@ -1,9 +1,9 @@
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace Kerobot.Modules.EntryRole
namespace RegexBot.Modules.EntryRole
{
/// <summary>
/// Contains configuration data as well as per-guild timers for those awaiting role assignment.

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>RegexBot.Modules</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>NoiTheCat</Authors>
<Description>A set of standard modules for use with RegexBot.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RegexBot\RegexBot.csproj" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,6 @@
using Discord;
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
@ -8,13 +8,13 @@ using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
namespace Kerobot.Modules.RegexModerator
namespace RegexBot.Modules.RegexModerator
{
/// <summary>
/// Representation of a single RegexModerator rule for a guild.
/// Data in this class is immutable. Contains various helper methods.
/// </summary>
[DebuggerDisplay("RM rule '{_label}' for {_guild}")]
[DebuggerDisplay("RM rule '{Label}'")]
class ConfDefinition
{
public string Label { get; }
@ -32,10 +32,11 @@ namespace Kerobot.Modules.RegexModerator
public EntityName RoleRemove { get; }
//readonly bool _rRolePersist; // TODO use when feature exists
public EntityName ReportingChannel { get; }
public Kerobot.RemovalType RemovalAction { get; } // ban, kick?
public RegexBot.RemovalType RemoveAction { get; } // ban, kick?
public int BanPurgeDays { get; }
public string RemovalReason { get; } // reason to place into audit log, notification
public bool RemovalSendUserNotification; // send ban/kick notification to user?
public string RemoveReason { get; } // reason to place into audit log and notification
public bool RemoveAnnounce { get; } // send success/failure message in invoking channel? default: true
public bool RemoveNotifyTarget { get; } // send ban/kick notification to user?
public bool DeleteMessage { get; }
public ConfDefinition(JObject def)
@ -56,7 +57,7 @@ namespace Kerobot.Modules.RegexModerator
const string ErrNoRegex = "Regular expression patterns are not defined";
var rxs = new List<Regex>();
var rxconf = def["regex"];
var rxconf = def["Regex"];
if (rxconf == null) throw new ModuleLoadException(ErrNoRegex + errpostfx);
if (rxconf.Type == JTokenType.Array)
{
@ -140,20 +141,19 @@ namespace Kerobot.Modules.RegexModerator
}
else ReportingChannel = null;
var removestr = resp[nameof(RemovalAction)]?.Value<string>();
var removestr = resp[nameof(RemoveAction)]?.Value<string>();
// accept values ban, kick, none
switch (removestr)
{
case "ban": RemovalAction = Kerobot.RemovalType.Ban; break;
case "kick": RemovalAction = Kerobot.RemovalType.Kick; break;
case "none": RemovalAction = Kerobot.RemovalType.None; break;
case "ban": RemoveAction = RegexBot.RemovalType.Ban; break;
case "kick": RemoveAction = RegexBot.RemovalType.Kick; break;
case "none": RemoveAction = RegexBot.RemovalType.None; break;
default:
if (removestr != null)
throw new ModuleLoadException("RemoveAction is not set to a proper value" + errpostfx);
break;
}
// TODO extract BanPurgeDays
int? banpurgeint;
try { banpurgeint = resp[nameof(BanPurgeDays)]?.Value<int>(); }
catch (InvalidCastException) { throw new ModuleLoadException("BanPurgeDays must be a numeric value."); }
@ -164,9 +164,11 @@ namespace Kerobot.Modules.RegexModerator
BanPurgeDays = banpurgeint ?? 0;
}
RemovalReason = resp[nameof(RemovalReason)]?.Value<string>();
RemoveReason = resp[nameof(RemoveReason)]?.Value<string>();
RemovalSendUserNotification = resp[nameof(RemovalSendUserNotification)]?.Value<bool>() ?? false;
RemoveAnnounce = resp[nameof(RemoveAnnounce)]?.Value<bool>() ?? true;
RemoveNotifyTarget = resp[nameof(RemoveNotifyTarget)]?.Value<bool>() ?? false;
DeleteMessage = resp[nameof(DeleteMessage)]?.Value<bool>() ?? false;
}
@ -188,7 +190,7 @@ namespace Kerobot.Modules.RegexModerator
// TODO enforce maximum execution time
// TODO multi-processing of multiple regexes?
// TODO metrics: temporary tracking of regex execution times
if (regex.IsMatch(m.Content)) return true;
if (regex.IsMatch(matchText)) return true;
}
return false;

View file

@ -3,16 +3,16 @@ using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Kerobot.Modules.RegexModerator
namespace RegexBot.Modules.RegexModerator
{
/// <summary>
/// The 'star' feature of Kerobot. Users define pattern-based rules with other constraints.
/// When triggered, each rule executes one or more different actions.
/// </summary>
[KerobotModule]
[RegexbotModule]
public class RegexModerator : ModuleBase
{
public RegexModerator(Kerobot kb) : base(kb)
public RegexModerator(RegexbotClient bot) : base(bot)
{
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
@ -23,6 +23,9 @@ namespace Kerobot.Modules.RegexModerator
if (config == null) return Task.FromResult<object>(null);
var defs = new List<ConfDefinition>();
if (config.Type != JTokenType.Array)
throw new ModuleLoadException(this.Name + " configuration must be a JSON array.");
// TODO better error reporting during this process
foreach (var def in config.Children<JObject>())
defs.Add(new ConfDefinition(def));
@ -41,6 +44,13 @@ namespace Kerobot.Modules.RegexModerator
/// </summary>
private async Task ReceiveIncomingMessage(SocketMessage msg)
{
if (msg.Author.Id == 0)
{
// TODO what changed to cause this? this wasn't happening before.
System.Console.WriteLine($"Skip processing of message with empty metadata. Msg ID {msg.Id} - Msg content: {msg.Content} - Embed count: {msg.Embeds.Count}");
return;
}
// Ignore non-guild channels
if (!(msg.Channel is SocketGuildChannel ch)) return;
@ -50,8 +60,6 @@ namespace Kerobot.Modules.RegexModerator
// Send further processing to thread pool.
// Match checking is a CPU-intensive task, thus very little checking is done here.
var msgProcessingTasks = new List<Task>();
foreach (var item in defs)
{
@ -75,7 +83,7 @@ namespace Kerobot.Modules.RegexModerator
// TODO logging options for match result; handle here?
var executor = new ResponseExecutor(def, Kerobot);
var executor = new ResponseExecutor(def, BotClient);
await executor.Execute(msg);
}
}

View file

@ -1,13 +1,13 @@
using Discord;
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using static Kerobot.Kerobot;
using static RegexBot.RegexbotClient;
namespace Kerobot.Modules.RegexModerator
namespace RegexBot.Modules.RegexModerator
{
/// <summary>
/// Helper class to RegexModerator that executes the appropriate actions associated with a triggered rule.
@ -15,13 +15,13 @@ namespace Kerobot.Modules.RegexModerator
class ResponseExecutor
{
private readonly ConfDefinition _rule;
private readonly Kerobot _bot;
private readonly RegexbotClient _bot;
private List<(ResponseAction, ResponseExecutionResult)> _results;
public ResponseExecutor(ConfDefinition rule, Kerobot kb)
public ResponseExecutor(ConfDefinition rule, RegexbotClient bot)
{
_rule = rule;
_bot = kb;
_bot = bot;
}
public async Task Execute(SocketMessage msg)
@ -68,18 +68,33 @@ namespace Kerobot.Modules.RegexModerator
private Task<ResponseExecutionResult> DoBan(SocketGuild g, SocketMessage msg)
{
if (_rule.RemovalAction != RemovalType.Ban) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemovalAction);
if (_rule.RemoveAction != RemovalType.Ban) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemoveAction);
}
private Task<ResponseExecutionResult> DoKick(SocketGuild g, SocketMessage msg)
{
if (_rule.RemovalAction != RemovalType.Kick) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemovalAction);
if (_rule.RemoveAction != RemovalType.Kick) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemoveAction);
}
private async Task<ResponseExecutionResult> DoBanOrKick(SocketGuild g, SocketMessage msg, RemovalType t)
{
var result = await _bot.BanOrKickAsync(t, g, $"Rule '{_rule.Label}'",
msg.Author.Id, _rule.BanPurgeDays, _rule.RemovalReason, _rule.RemovalSendUserNotification);
msg.Author.Id, _rule.BanPurgeDays, _rule.RemoveReason, _rule.RemoveNotifyTarget);
string logAnnounce = null;
if (_rule.RemoveAnnounce)
{
try
{
await msg.Channel.SendMessageAsync(result.GetResultString(_bot, g.Id));
}
catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
logAnnounce = "Could not send " + (t == RemovalType.Ban ? "ban" : "kick") + " announcement to channel "
+ "due to a permissions issue.";
}
}
if (result.ErrorForbidden)
{
return new ResponseExecutionResult(false, ForbiddenGenericError);
@ -88,10 +103,7 @@ namespace Kerobot.Modules.RegexModerator
{
return new ResponseExecutionResult(false, "The target user is no longer in the server.");
}
else return new ResponseExecutionResult(true, null);
// TODO option to announce ban/kick result in the trigger channel
// ^ implementation: take result, reply to channel. don't alter BanOrKickAsync.
else return new ResponseExecutionResult(true, logAnnounce);
}
private async Task<ResponseExecutionResult> DoDelete(SocketGuild g, SocketMessage msg)

34
RegexBot.sln Normal file
View file

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegexBot", "RegexBot\RegexBot.csproj", "{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegexBot-Modules", "RegexBot-Modules\RegexBot-Modules.csproj", "{03111D82-30ED-4FDF-A512-87BDC05C6DA1}"
ProjectSection(ProjectDependencies) = postProject
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3} = {6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3A92F-F1FC-4BA8-8018-1A05CB4C7FA3}.Release|Any CPU.Build.0 = Release|Any CPU
{03111D82-30ED-4FDF-A512-87BDC05C6DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03111D82-30ED-4FDF-A512-87BDC05C6DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03111D82-30ED-4FDF-A512-87BDC05C6DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03111D82-30ED-4FDF-A512-87BDC05C6DA1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C823EC9C-CF20-4437-8B99-72158A6DD113}
EndGlobalSection
EndGlobal

View file

@ -5,7 +5,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Kerobot.Common
namespace RegexBot.Common
{
/// <summary>
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values.

View file

@ -4,7 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Kerobot.Common
namespace RegexBot.Common
{
/// <summary>
/// The type of entity specified in an <see cref="EntityName"/>.

View file

@ -2,7 +2,7 @@
using Newtonsoft.Json.Linq;
using System;
namespace Kerobot.Common
namespace RegexBot.Common
{
/// <summary>
/// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions.

View file

@ -1,12 +1,12 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RegexBot;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Kerobot
{
namespace RegexBot {
/// <summary>
/// Contains instance configuration for this bot,
/// including Discord connection settings and service configuration.

View file

@ -1,12 +1,12 @@
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using static Kerobot.Kerobot;
using static RegexBot.RegexbotClient;
namespace Kerobot
namespace RegexBot
{
/// <summary>
/// Base class for a Kerobot module. A module implements a user-facing feature and is expected to directly handle
@ -21,18 +21,18 @@ namespace Kerobot
/// <summary>
/// Retrieves the Kerobot instance.
/// </summary>
public Kerobot Kerobot { get; }
public RegexbotClient BotClient { get; }
/// <summary>
/// Retrieves the Discord client instance.
/// </summary>
public DiscordSocketClient DiscordClient { get => Kerobot.DiscordClient; }
public DiscordSocketClient DiscordClient { get => BotClient.DiscordClient; }
/// <summary>
/// When a module is loaded, this constructor is called.
/// Services are available at this point. Do not attempt to communicate to Discord within the constructor.
/// </summary>
public ModuleBase(Kerobot kb) => Kerobot = kb;
public ModuleBase(RegexbotClient bot) => BotClient = bot;
/// <summary>
/// Gets the module name.
@ -61,7 +61,7 @@ namespace Kerobot
/// Thrown if the stored state object cannot be cast as specified.
/// </exception>
[DebuggerStepThrough]
protected T GetGuildState<T>(ulong guildId) => Kerobot.GetGuildState<T>(guildId, GetType());
protected T GetGuildState<T>(ulong guildId) => BotClient.GetGuildState<T>(guildId, GetType());
/// <summary>
/// Appends a message to the global instance log. Use sparingly.
@ -70,12 +70,12 @@ namespace Kerobot
/// Specifies if the log message should be sent to the reporting channel.
/// Only messages of very high importance should use this option.
/// </param>
protected Task LogAsync(string message, bool report = false) => Kerobot.InstanceLogAsync(report, Name, message);
protected Task LogAsync(string message, bool report = false) => BotClient.InstanceLogAsync(report, Name, message);
/// <summary>
/// Appends a message to the log for the specified guild.
/// </summary>
protected Task LogAsync(ulong guild, string message) => Kerobot.GuildLogAsync(guild, Name, message);
protected Task LogAsync(ulong guild, string message) => BotClient.GuildLogAsync(guild, Name, message);
/// <summary>
/// Attempts to ban the given user from the specified guild. It is greatly preferred to call this method
@ -90,7 +90,7 @@ namespace Kerobot
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action being taken.</param>
protected Task<BanKickResult> BanAsync(SocketGuild guild, string source, ulong targetUser, int purgeDays, string reason, bool sendDMToTarget)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, sendDMToTarget);
=> BotClient.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an
@ -99,8 +99,8 @@ namespace Kerobot
/// <param name="targetSearch">The EntityCache search string.</param>
protected async Task<BanKickResult> BanAsync(SocketGuild guild, string source, string targetSearch, int purgeDays, string reason, bool sendDMToTarget)
{
var result = await Kerobot.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true);
var result = await BotClient.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true, RemovalType.Ban, 0);
return await BanAsync(guild, source, result.UserID, purgeDays, reason, sendDMToTarget);
}
@ -116,7 +116,7 @@ namespace Kerobot
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action being taken.</param>
protected Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, bool sendDMToTarget)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget);
=> BotClient.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an
@ -125,8 +125,8 @@ namespace Kerobot
/// <param name="targetSearch">The EntityCache search string.</param>
protected async Task<BanKickResult> KickAsync(SocketGuild guild, string source, string targetSearch, string reason, bool sendDMToTarget)
{
var result = await Kerobot.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true);
var result = await BotClient.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
return await KickAsync(guild, source, result.UserID, reason, sendDMToTarget);
}
@ -137,7 +137,7 @@ namespace Kerobot
/// An <see cref="EntityList"/> with corresponding moderator configuration data.
/// In case none exists, an empty list will be returned.
/// </returns>
protected EntityList GetModerators(ulong guild) => Kerobot.GetModerators(guild);
protected EntityList GetModerators(ulong guild) => BotClient.GetModerators(guild);
}
/// <summary>

View file

@ -5,7 +5,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
namespace Kerobot
namespace RegexBot
{
static class ModuleLoader
{
@ -14,7 +14,7 @@ namespace Kerobot
/// <summary>
/// Given the instance configuration, loads all appropriate types from file specified in it.
/// </summary>
internal static ReadOnlyCollection<ModuleBase> Load(InstanceConfig conf, Kerobot k)
internal static ReadOnlyCollection<ModuleBase> Load(InstanceConfig conf, RegexbotClient k)
{
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
var modules = new List<ModuleBase>();
@ -50,11 +50,11 @@ namespace Kerobot
return modules.AsReadOnly();
}
static IEnumerable<ModuleBase> LoadModulesFromAssembly(Assembly asm, Kerobot k)
static IEnumerable<ModuleBase> LoadModulesFromAssembly(Assembly asm, RegexbotClient k)
{
var eligibleTypes = from type in asm.GetTypes()
where !type.IsAssignableFrom(typeof(ModuleBase))
where type.GetCustomAttribute<KerobotModuleAttribute>() != null
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
select type;
k.InstanceLogAsync(false, LogName, $"Scanning {asm.GetName().Name}");

View file

@ -2,13 +2,11 @@
using CommandLine.Text;
using System;
namespace Kerobot
{
namespace RegexBot {
/// <summary>
/// Command line options
/// </summary>
class Options
{
class Options {
[Option('c', "config", Default = null,
HelpText = "Custom path to instance configuration. Defaults to config.json in bot directory.")]
public string ConfigFile { get; set; }
@ -16,19 +14,17 @@ namespace Kerobot
/// <summary>
/// Command line arguments parsed here. Depending on inputs, the program can exit here.
/// </summary>
public static Options ParseOptions(string[] args)
{
public static Options ParseOptions(string[] args) {
// Parser will not write out to console by itself
var parser = new Parser(config => config.HelpWriter = null);
Options opts = null;
var result = parser.ParseArguments<Options>(args);
result.WithParsed(p => opts = p);
result.WithNotParsed(p =>
{
result.WithNotParsed(p => {
// Taking some extra steps to modify the header to make it resemble our welcome message.
var ht = HelpText.AutoBuild(result);
ht.Heading = ht.Heading += " - https://github.com/Noikoio/Kerobot";
ht.Heading += " - https://github.com/NoiTheCat/RegexBot";
Console.WriteLine(ht.ToString());
Environment.Exit(1);
});

View file

@ -1,10 +1,10 @@
using Discord;
using Discord.WebSocket;
using RegexBot;
using System;
using System.Threading.Tasks;
namespace Kerobot
{
namespace RegexBot {
/// <summary>
/// Program startup class.
/// </summary>
@ -15,7 +15,7 @@ namespace Kerobot
/// </summary>
public static DateTimeOffset StartTime { get; private set; }
static Kerobot _main;
static RegexbotClient _main;
/// <summary>
/// Entry point. Loads options, initializes all components, then connects to Discord.
@ -64,7 +64,7 @@ namespace Kerobot
});
// Kerobot class initialization - will set up services and modules
_main = new Kerobot(cfg, client);
_main = new RegexbotClient(cfg, client);
// Set up application close handler
Console.CancelKeyPress += Console_CancelKeyPress;
@ -82,7 +82,7 @@ namespace Kerobot
{
e.Cancel = true;
_main.InstanceLogAsync(true, "Kerobot", "Shutting down. Reason: Interrupt signal.");
_main.InstanceLogAsync(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
// 5 seconds of leeway - any currently running tasks will need time to finish executing
var leeway = Task.Delay(5000);
@ -93,7 +93,7 @@ namespace Kerobot
leeway.Wait();
bool success = _main.DiscordClient.StopAsync().Wait(1000);
if (!success) _main.InstanceLogAsync(false, "Kerobot",
if (!success) _main.InstanceLogAsync(false, nameof(RegexBot),
"Failed to disconnect cleanly from Discord. Will force shut down.").Wait();
Environment.Exit(0);
}

View file

@ -2,13 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<StartupObject>Kerobot.Program</StartupObject>
<Authors>Noiiko</Authors>
<Company />
<TargetFramework>net6.0</TargetFramework>
<Authors>NoiTheCat</Authors>
<Description>Advanced and flexible Discord moderation bot.</Description>
<Version>0.0.1</Version>
<LangVersion>7.2</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@ -25,10 +24,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.5.0" />
<PackageReference Include="Discord.Net" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Npgsql" Version="4.0.7" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Discord.Net" Version="3.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Npgsql" Version="6.0.3" />
</ItemGroup>
</Project>

View file

@ -1,16 +1,16 @@
using Discord.WebSocket;
using Kerobot.Services;
using RegexBot.Services;
using Npgsql;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Kerobot
namespace RegexBot
{
/// <summary>
/// Kerobot main class, and the most accessible and useful class in the whole program.
/// Provides an interface for any part of the program to call into all existing services.
/// </summary>
public partial class Kerobot
public partial class RegexbotClient
{
/// <summary>
/// Gets application instance configuration.
@ -32,7 +32,7 @@ namespace Kerobot
/// </summary>
internal IReadOnlyCollection<ModuleBase> Modules { get; }
internal Kerobot(InstanceConfig conf, DiscordSocketClient client)
internal RegexbotClient(InstanceConfig conf, DiscordSocketClient client)
{
Config = conf;
DiscordClient = client;
@ -45,8 +45,8 @@ namespace Kerobot
// Everything's ready to go. Print the welcome message here.
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
InstanceLogAsync(false, "Kerobot",
$"This is Kerobot v{ver.ToString(3)}. https://github.com/Noiiko/Kerobot").Wait();
InstanceLogAsync(false, nameof(RegexBot),
$"This is RegexBot v{ver.ToString(3)}. https://github.com/NoiTheCat/RegexBot").Wait();
// We return to Program.cs at this point.
}

View file

@ -1,6 +1,6 @@
using System;
namespace Kerobot
namespace RegexBot
{
/// <summary>
/// Specifies to the Kerobot module loader that the target class should be treated as a module instance.
@ -8,5 +8,5 @@ namespace Kerobot
/// the program searches for classes implementing <see cref="ModuleBase"/> that also contain this attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class KerobotModuleAttribute : Attribute { }
public class RegexbotModuleAttribute : Attribute { }
}

View file

@ -0,0 +1,132 @@
using Discord.Net;
using static RegexBot.RegexbotClient;
namespace RegexBot
{
// Instances of this are created by CommonFunctionService and by ModuleBase on behalf of CommonFunctionService,
// and are meant to be sent to modules. This class has therefore been put within the Kerobot namespace.
/// <summary>
/// Contains information on various success/failure outcomes for a ban or kick operation.
/// </summary>
public class BanKickResult
{
private readonly bool _userNotFound; // possible to receive this error by means other than exception
private readonly RemovalType _rptRemovalType;
private readonly ulong _rptTargetId;
internal BanKickResult(HttpException error, bool notificationSuccess, bool errorNotFound,
RemovalType rtype, ulong rtarget)
{
OperationError = error;
MessageSendSuccess = notificationSuccess;
_userNotFound = errorNotFound;
_rptRemovalType = rtype;
_rptTargetId = rtarget;
}
/// <summary>
/// Gets a value indicating whether the kick or ban succeeded.
/// </summary>
public bool OperationSuccess {
get {
if (ErrorNotFound) return false;
if (OperationError != null) return false;
return true;
}
}
/// <summary>
/// The exception thrown, if any, when attempting to kick or ban the target.
/// </summary>
public HttpException OperationError { get; }
/// <summary>
/// Indicates if the operation failed due to being unable to find the user.
/// </summary>
/// <remarks>
/// This may return true even if <see cref="OperationError"/> returns null.
/// This type of error may appear in cases that do not involve an exception being thrown.
/// </remarks>
public bool ErrorNotFound
{
get
{
if (_userNotFound) return true;
if (OperationError != null) return OperationError.HttpCode == System.Net.HttpStatusCode.NotFound;
return false;
}
}
/// <summary>
/// Indicates if the operation failed due to a permissions issue.
/// </summary>
public bool ErrorForbidden
{
get
{
if (OperationSuccess) return false;
return OperationError.HttpCode == System.Net.HttpStatusCode.Forbidden;
}
}
/// <summary>
/// Gets a value indicating whether the user was able to receive the ban or kick message.
/// </summary>
/// <value>
/// <see langword="false"/> if an error was encountered when attempting to send the target a DM. Will always
/// return <see langword="true"/> otherwise, including cases in which no message was sent.
/// </value>
public bool MessageSendSuccess { get; }
/// <summary>
/// Returns a message representative of the ban/kick result that may be posted as-is
/// within the a Discord channel.
/// </summary>
public string GetResultString(RegexbotClient bot, ulong guildId)
{
string msg;
if (OperationSuccess) msg = ":white_check_mark: ";
else msg = ":x: Failed to ";
if (_rptRemovalType == RemovalType.Ban)
{
if (OperationSuccess) msg += "Banned";
else msg += "ban";
}
else if (_rptRemovalType == RemovalType.Kick)
{
if (OperationSuccess) msg += "Kicked";
else msg += "kick";
}
else
{
throw new System.InvalidOperationException("Cannot create a message for removal type of None.");
}
if (_rptTargetId != 0)
{
var user = bot.EcQueryUser(guildId, _rptTargetId.ToString()).GetAwaiter().GetResult();
if (user != null)
{
// TODO sanitize possible formatting characters in display name
msg += $" user **{user.Username}#{user.Discriminator}**";
}
}
if (OperationSuccess)
{
msg += ".";
if (!MessageSendSuccess) msg += "\n(User was unable to receive notification message.)";
}
else
{
if (ErrorNotFound) msg += ": The specified user could not be found.";
else if (ErrorForbidden) msg += ": I do not have the required permissions to perform that action.";
}
return msg;
}
}
}

View file

@ -2,9 +2,9 @@
using System.Threading.Tasks;
using Discord.Net;
using Discord.WebSocket;
using static Kerobot.Kerobot;
using static RegexBot.RegexbotClient;
namespace Kerobot.Services.CommonFunctions
namespace RegexBot.Services.CommonFunctions
{
/// <summary>
/// Implements certain common actions that modules may want to perform. Using this service to perform those
@ -16,7 +16,7 @@ namespace Kerobot.Services.CommonFunctions
/// </summary>
internal class CommonFunctionsService : Service
{
public CommonFunctionsService(Kerobot kb) : base(kb) { }
public CommonFunctionsService(RegexbotClient bot) : base(bot) { }
#region Guild member removal
/// <summary>
@ -33,13 +33,9 @@ namespace Kerobot.Services.CommonFunctions
SocketGuildUser utarget = guild.GetUser(target);
// Can't kick without obtaining user object. Quit here.
if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true);
if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
#if DEBUG
#warning "Services are NOT NOTIFIED" of bans/kicks during debug."
#else
// TODO notify services here as soon as we get some who will want to listen to this
#endif
// Send DM notification
if (sendDmToTarget)
@ -51,23 +47,19 @@ namespace Kerobot.Services.CommonFunctions
// Perform the action
try
{
#if DEBUG
#warning "Actual kick/ban is DISABLED during debug."
#else
if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays);
else await utarget.KickAsync(logReason);
// TODO !! Insert ban reason! For kick also: Figure out a way to specify invoker.
#endif
}
catch (HttpException ex)
{
return new BanKickResult(ex, dmSuccess, false);
return new BanKickResult(ex, dmSuccess, false, t, target);
}
return new BanKickResult(null, dmSuccess, false);
return new BanKickResult(null, dmSuccess, false, t, target);
}
private async Task<bool> BanKickSendNotificationAsync(SocketGuildUser target, Kerobot.RemovalType action, string reason)
private async Task<bool> BanKickSendNotificationAsync(SocketGuildUser target, RemovalType action, string reason)
{
const string DMTemplate = "You have been {0} from {1} for the following reason:\n{2}";

View file

@ -1,10 +1,10 @@
using Discord.WebSocket;
using System.Threading.Tasks;
using Kerobot.Services.CommonFunctions;
using RegexBot.Services.CommonFunctions;
namespace Kerobot
namespace RegexBot
{
partial class Kerobot
partial class RegexbotClient
{
private CommonFunctionsService _svcCommonFunctions;

View file

@ -1,7 +1,7 @@
using System;
using System.Data.Common;
namespace Kerobot // Publicly accessible class; placing in main namespace
namespace RegexBot // Publicly accessible class; placing in main namespace
{
/// <summary>
/// Representation of user information retrieved from Kerobot's UserCache.

View file

@ -1,6 +1,6 @@
using System.Threading.Tasks;
namespace Kerobot.Services.EntityCache
namespace RegexBot.Services.EntityCache
{
/// <summary>
/// Provides and maintains a database-backed cache of entities. Portions of information collected by this
@ -11,14 +11,14 @@ namespace Kerobot.Services.EntityCache
{
private readonly UserCache _uc;
internal EntityCacheService(Kerobot kb) : base(kb)
internal EntityCacheService(RegexbotClient bot) : base(bot)
{
// Currently we only have UserCache. May add Channel and Server caches later.
_uc = new UserCache(kb);
_uc = new UserCache(bot);
}
/// <summary>
/// See <see cref="Kerobot.EcQueryUser(ulong, string)"/>.
/// See <see cref="RegexbotClient.EcQueryUser(ulong, string)"/>.
/// </summary>
internal Task<CachedUser> QueryUserCache(ulong guildId, string search) => _uc.Query(guildId, search);
}

View file

@ -1,9 +1,9 @@
using Kerobot.Services.EntityCache;
using RegexBot.Services.EntityCache;
using System.Threading.Tasks;
namespace Kerobot
namespace RegexBot
{
partial class Kerobot
partial class RegexbotClient
{
private EntityCacheService _svcEntityCache;

View file

@ -4,7 +4,7 @@ using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Kerobot.Services.EntityCache
namespace RegexBot.Services.EntityCache
{
/// <summary>
/// Provides and maintains a database-backed cache of users.
@ -13,15 +13,15 @@ namespace Kerobot.Services.EntityCache
/// </summary>
class UserCache
{
private Kerobot _kb;
private RegexbotClient _bot;
internal UserCache(Kerobot kb)
internal UserCache(RegexbotClient bot)
{
_kb = kb;
_bot = bot;
CreateDatabaseTablesAsync().Wait();
kb.DiscordClient.GuildMemberUpdated += DiscordClient_GuildMemberUpdated;
kb.DiscordClient.UserUpdated += DiscordClient_UserUpdated;
bot.DiscordClient.GuildMemberUpdated += DiscordClient_GuildMemberUpdated;
bot.DiscordClient.UserUpdated += DiscordClient_UserUpdated;
}
#region Database setup
@ -31,7 +31,7 @@ namespace Kerobot.Services.EntityCache
private async Task CreateDatabaseTablesAsync()
{
using (var db = await _kb.GetOpenNpgsqlConnectionAsync())
using (var db = await _bot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -80,7 +80,7 @@ namespace Kerobot.Services.EntityCache
#region Database updates
private async Task DiscordClient_UserUpdated(SocketUser old, SocketUser current)
{
using (var db = await _kb.GetOpenNpgsqlConnectionAsync())
using (var db = await _bot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -109,7 +109,7 @@ namespace Kerobot.Services.EntityCache
// Also update user data here, in case it's unknown (avoid foreign key constraint violation)
await DiscordClient_UserUpdated(old, current);
using (var db = await _kb.GetOpenNpgsqlConnectionAsync())
using (var db = await _bot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -135,7 +135,7 @@ namespace Kerobot.Services.EntityCache
private static readonly Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
/// <summary>
/// See <see cref="Kerobot.EcQueryUser(ulong, string)"/>.
/// See <see cref="RegexbotClient.EcQueryUser(ulong, string)"/>.
/// </summary>
internal async Task<CachedUser> Query(ulong guildID, string search)
{
@ -171,7 +171,7 @@ namespace Kerobot.Services.EntityCache
private async Task<CachedUser> InternalDoQuery(ulong guildId, ulong? sID, string sName, string sDisc)
{
using (var db = await _kb.GetOpenNpgsqlConnectionAsync())
using (var db = await _bot.GetOpenNpgsqlConnectionAsync())
{
var c = db.CreateCommand();
c.CommandText = $"select * from {UserView} " +

View file

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Discord;
using NpgsqlTypes;
namespace Kerobot.Services.EventLogging
namespace RegexBot.Services.EventLogging
{
/// <summary>
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
@ -16,17 +16,17 @@ namespace Kerobot.Services.EventLogging
// Note: Service.Log's functionality is implemented here. Don't use it within this class.
// If necessary, use DoInstanceLogAsync instead.
internal EventLoggingService(Kerobot kb) : base(kb)
internal EventLoggingService(RegexbotClient bot) : base(bot)
{
// Create logging table
CreateDatabaseTablesAsync().Wait();
// Discord.Net log handling (client logging option is specified in Program.cs)
kb.DiscordClient.Log += DiscordClient_Log;
bot.DiscordClient.Log += DiscordClient_Log;
// Ready message too
kb.DiscordClient.Ready +=
async delegate { await DoInstanceLogAsync(true, "Kerobot", "Connected and ready."); };
bot.DiscordClient.Ready +=
async delegate { await DoInstanceLogAsync(true, nameof(RegexBot), "Connected and ready."); };
}
/// <summary>
@ -48,7 +48,7 @@ namespace Kerobot.Services.EventLogging
const string TableLog = "program_log";
private async Task CreateDatabaseTablesAsync()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
using (var db = await BotClient.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -72,7 +72,7 @@ namespace Kerobot.Services.EventLogging
}
private async Task TableInsertAsync(ulong guildId, DateTimeOffset timestamp, string source, string message)
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
using (var db = await BotClient.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -104,7 +104,7 @@ namespace Kerobot.Services.EventLogging
}
/// <summary>
/// See <see cref="Kerobot.InstanceLogAsync(bool, string, string)"/>
/// See <see cref="RegexbotClient.InstanceLogAsync(bool, string, string)"/>
/// </summary>
public async Task DoInstanceLogAsync(bool report, string source, string message)
{
@ -127,11 +127,11 @@ namespace Kerobot.Services.EventLogging
// Report to logging channel if necessary and possible
// TODO replace with webhook?
var (g, c) = Kerobot.Config.InstanceLogReportTarget;
var (g, c) = BotClient.Config.InstanceLogReportTarget;
if ((insertException != null || report) &&
g != 0 && c != 0 && Kerobot.DiscordClient.ConnectionState == ConnectionState.Connected)
g != 0 && c != 0 && BotClient.DiscordClient.ConnectionState == ConnectionState.Connected)
{
var ch = Kerobot.DiscordClient.GetGuild(g)?.GetTextChannel(c);
var ch = BotClient.DiscordClient.GetGuild(g)?.GetTextChannel(c);
if (ch == null) return; // not connected, or channel doesn't exist.
if (insertException != null)
@ -176,13 +176,16 @@ namespace Kerobot.Services.EventLogging
}
/// <summary>
/// See <see cref="Kerobot.GuildLogAsync(ulong, string, string)"/>
/// See <see cref="RegexbotClient.GuildLogAsync(ulong, string, string)"/>
/// </summary>
public async Task DoGuildLogAsync(ulong guild, string source, string message)
{
try
{
await TableInsertAsync(guild, DateTimeOffset.UtcNow, source, message);
#if DEBUG
FormatToConsole(DateTimeOffset.UtcNow, $"DEBUG {guild} - {source}", message);
#endif
}
catch (Exception ex)
{

View file

@ -1,9 +1,9 @@
using Kerobot.Services.EventLogging;
using RegexBot.Services.EventLogging;
using System.Threading.Tasks;
namespace Kerobot
namespace RegexBot
{
partial class Kerobot
partial class RegexbotClient
{
EventLoggingService _svcLogging;

View file

@ -4,11 +4,11 @@ using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Discord.WebSocket;
using Kerobot.Common;
using RegexBot.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Kerobot.Services.GuildState
namespace RegexBot.Services.GuildState
{
/// <summary>
/// Implements per-module storage and retrieval of guild-specific state data.
@ -22,15 +22,15 @@ namespace Kerobot.Services.GuildState
const string GuildLogSource = "Configuration loader";
public GuildStateService(Kerobot kb) : base(kb)
public GuildStateService(RegexbotClient bot) : base(bot)
{
_moderators = new Dictionary<ulong, EntityList>();
_states = new Dictionary<ulong, Dictionary<Type, StateInfo>>();
CreateDatabaseTablesAsync().Wait();
kb.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
kb.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
kb.DiscordClient.LeftGuild += DiscordClient_LeftGuild;
bot.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
bot.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
bot.DiscordClient.LeftGuild += DiscordClient_LeftGuild;
// TODO periodic task for refreshing stale configuration
}
@ -57,12 +57,12 @@ namespace Kerobot.Services.GuildState
bool success = await LoadGuildConfiguration(arg.Id);
if (!success)
{
await Kerobot.GuildLogAsync(arg.Id, GuildLogSource,
await BotClient.GuildLogAsync(arg.Id, GuildLogSource,
"Configuration was not reloaded due to the previously stated error(s).");
}
else
{
await Kerobot.InstanceLogAsync(false, GuildLogSource,
await BotClient.InstanceLogAsync(false, GuildLogSource,
$"Configuration successfully refreshed for guild ID {arg.Id}.");
}
}
@ -129,7 +129,7 @@ namespace Kerobot.Services.GuildState
}
catch (Exception ex) when (ex is JsonReaderException || ex is InvalidCastException)
{
await Kerobot.GuildLogAsync(guildId, GuildLogSource,
await BotClient.GuildLogAsync(guildId, GuildLogSource,
$"A problem exists within the guild configuration: {ex.Message}");
// Don't update currently loaded state.
@ -143,7 +143,7 @@ namespace Kerobot.Services.GuildState
// Create guild state objects for all existing modules
var newStates = new Dictionary<Type, StateInfo>();
foreach (var mod in Kerobot.Modules)
foreach (var mod in BotClient.Modules)
{
var t = mod.GetType();
var tn = t.Name;
@ -158,9 +158,9 @@ namespace Kerobot.Services.GuildState
{
Log("Unhandled exception while initializing guild state for module:\n" +
$"Module: {tn} | " +
$"Guild: {guildId} ({Kerobot.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
$"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
$"```\n{ex.ToString()}\n```", true).Wait();
Kerobot.GuildLogAsync(guildId, GuildLogSource,
BotClient.GuildLogAsync(guildId, GuildLogSource,
"An internal error occurred when attempting to load new configuration. " +
"The bot owner has been notified.").Wait();
return false;
@ -168,7 +168,7 @@ namespace Kerobot.Services.GuildState
}
catch (ModuleLoadException ex)
{
await Kerobot.GuildLogAsync(guildId, GuildLogSource,
await BotClient.GuildLogAsync(guildId, GuildLogSource,
$"{tn} has encountered an issue with its configuration: {ex.Message}");
return false;
}
@ -188,7 +188,7 @@ namespace Kerobot.Services.GuildState
/// </summary>
private async Task CreateDatabaseTablesAsync()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
using (var db = await BotClient.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
@ -241,7 +241,7 @@ namespace Kerobot.Services.GuildState
/// </summary>
private string GetDefaultConfiguration()
{
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
const string ResourceName = $"{nameof(RegexBot)}.DefaultGuildConfig.json";
var a = System.Reflection.Assembly.GetExecutingAssembly();
using (var s = a.GetManifestResourceStream(ResourceName))

View file

@ -1,10 +1,10 @@
using Kerobot.Common;
using Kerobot.Services.GuildState;
using RegexBot.Common;
using RegexBot.Services.GuildState;
using System;
namespace Kerobot
namespace RegexBot
{
partial class Kerobot
partial class RegexbotClient
{
private GuildStateService _svcGuildState;

View file

@ -1,7 +1,7 @@
using Newtonsoft.Json.Linq;
using System;
namespace Kerobot.Services.GuildState
namespace RegexBot.Services.GuildState
{
/// <summary>
/// Contains a guild state object and other useful metadata in regards to it.

View file

@ -1,6 +1,6 @@
using System.Threading.Tasks;
namespace Kerobot.Services
namespace RegexBot.Services
{
/// <summary>
/// Base class for Kerobot services.
@ -11,11 +11,11 @@ namespace Kerobot.Services
/// </remarks>
internal abstract class Service
{
public Kerobot Kerobot { get; }
public RegexbotClient BotClient { get; }
public string Name => this.GetType().Name;
public Service(Kerobot kb) => Kerobot = kb;
public Service(RegexbotClient bot) => BotClient = bot;
/// <summary>
/// Creates a log message.
@ -23,6 +23,6 @@ namespace Kerobot.Services
/// <param name="message">Logging message contents.</param>
/// <param name="report">Determines if the log message should be sent to a reporting channel.</param>
/// <returns></returns>
protected Task Log(string message, bool report = false) => Kerobot.InstanceLogAsync(report, Name, message);
protected Task Log(string message, bool report = false) => BotClient.InstanceLogAsync(report, Name, message);
}
}