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 # RegexBot
**This project is a major work in progress and is unusable in its current state. Not all features may be present.** **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 * Create rules based on regular expression patterns
* Follow up with custom responses ranging from sending a DM to disciplinary action * 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 * 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kerobot.Modules.AutoResponder namespace RegexBot.Modules.AutoResponder
{ {
/// <summary> /// <summary>
/// Provides the capability to define text responses to pattern-based triggers for fun or informational /// 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 /// 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. /// fit for non-moderation use cases and has specific features suitable to that end.
/// </summary> /// </summary>
[KerobotModule] [RegexbotModule]
class AutoResponder : ModuleBase class AutoResponder : ModuleBase
{ {
public AutoResponder(Kerobot kb) : base(kb) public AutoResponder(RegexbotClient bot) : base(bot)
{ {
DiscordClient.MessageReceived += DiscordClient_MessageReceived; DiscordClient.MessageReceived += DiscordClient_MessageReceived;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,16 +3,16 @@ using Newtonsoft.Json.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kerobot.Modules.RegexModerator namespace RegexBot.Modules.RegexModerator
{ {
/// <summary> /// <summary>
/// The 'star' feature of Kerobot. Users define pattern-based rules with other constraints. /// The 'star' feature of Kerobot. Users define pattern-based rules with other constraints.
/// When triggered, each rule executes one or more different actions. /// When triggered, each rule executes one or more different actions.
/// </summary> /// </summary>
[KerobotModule] [RegexbotModule]
public class RegexModerator : ModuleBase public class RegexModerator : ModuleBase
{ {
public RegexModerator(Kerobot kb) : base(kb) public RegexModerator(RegexbotClient bot) : base(bot)
{ {
DiscordClient.MessageReceived += DiscordClient_MessageReceived; DiscordClient.MessageReceived += DiscordClient_MessageReceived;
DiscordClient.MessageUpdated += DiscordClient_MessageUpdated; DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
@ -23,6 +23,9 @@ namespace Kerobot.Modules.RegexModerator
if (config == null) return Task.FromResult<object>(null); if (config == null) return Task.FromResult<object>(null);
var defs = new List<ConfDefinition>(); 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 // TODO better error reporting during this process
foreach (var def in config.Children<JObject>()) foreach (var def in config.Children<JObject>())
defs.Add(new ConfDefinition(def)); defs.Add(new ConfDefinition(def));
@ -41,6 +44,13 @@ namespace Kerobot.Modules.RegexModerator
/// </summary> /// </summary>
private async Task ReceiveIncomingMessage(SocketMessage msg) 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 // Ignore non-guild channels
if (!(msg.Channel is SocketGuildChannel ch)) return; if (!(msg.Channel is SocketGuildChannel ch)) return;
@ -50,8 +60,6 @@ namespace Kerobot.Modules.RegexModerator
// Send further processing to thread pool. // Send further processing to thread pool.
// Match checking is a CPU-intensive task, thus very little checking is done here. // Match checking is a CPU-intensive task, thus very little checking is done here.
var msgProcessingTasks = new List<Task>(); var msgProcessingTasks = new List<Task>();
foreach (var item in defs) foreach (var item in defs)
{ {
@ -75,7 +83,7 @@ namespace Kerobot.Modules.RegexModerator
// TODO logging options for match result; handle here? // TODO logging options for match result; handle here?
var executor = new ResponseExecutor(def, Kerobot); var executor = new ResponseExecutor(def, BotClient);
await executor.Execute(msg); await executor.Execute(msg);
} }
} }

View file

@ -1,13 +1,13 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Kerobot.Common; using RegexBot.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Kerobot.Kerobot; using static RegexBot.RegexbotClient;
namespace Kerobot.Modules.RegexModerator namespace RegexBot.Modules.RegexModerator
{ {
/// <summary> /// <summary>
/// Helper class to RegexModerator that executes the appropriate actions associated with a triggered rule. /// Helper class to RegexModerator that executes the appropriate actions associated with a triggered rule.
@ -15,13 +15,13 @@ namespace Kerobot.Modules.RegexModerator
class ResponseExecutor class ResponseExecutor
{ {
private readonly ConfDefinition _rule; private readonly ConfDefinition _rule;
private readonly Kerobot _bot; private readonly RegexbotClient _bot;
private List<(ResponseAction, ResponseExecutionResult)> _results; private List<(ResponseAction, ResponseExecutionResult)> _results;
public ResponseExecutor(ConfDefinition rule, Kerobot kb) public ResponseExecutor(ConfDefinition rule, RegexbotClient bot)
{ {
_rule = rule; _rule = rule;
_bot = kb; _bot = bot;
} }
public async Task Execute(SocketMessage msg) public async Task Execute(SocketMessage msg)
@ -68,18 +68,33 @@ namespace Kerobot.Modules.RegexModerator
private Task<ResponseExecutionResult> DoBan(SocketGuild g, SocketMessage msg) private Task<ResponseExecutionResult> DoBan(SocketGuild g, SocketMessage msg)
{ {
if (_rule.RemovalAction != RemovalType.Ban) return Task.FromResult<ResponseExecutionResult>(null); if (_rule.RemoveAction != RemovalType.Ban) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemovalAction); return DoBanOrKick(g, msg, _rule.RemoveAction);
} }
private Task<ResponseExecutionResult> DoKick(SocketGuild g, SocketMessage msg) private Task<ResponseExecutionResult> DoKick(SocketGuild g, SocketMessage msg)
{ {
if (_rule.RemovalAction != RemovalType.Kick) return Task.FromResult<ResponseExecutionResult>(null); if (_rule.RemoveAction != RemovalType.Kick) return Task.FromResult<ResponseExecutionResult>(null);
return DoBanOrKick(g, msg, _rule.RemovalAction); return DoBanOrKick(g, msg, _rule.RemoveAction);
} }
private async Task<ResponseExecutionResult> DoBanOrKick(SocketGuild g, SocketMessage msg, RemovalType t) private async Task<ResponseExecutionResult> DoBanOrKick(SocketGuild g, SocketMessage msg, RemovalType t)
{ {
var result = await _bot.BanOrKickAsync(t, g, $"Rule '{_rule.Label}'", 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) if (result.ErrorForbidden)
{ {
return new ResponseExecutionResult(false, ForbiddenGenericError); 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."); return new ResponseExecutionResult(false, "The target user is no longer in the server.");
} }
else return new ResponseExecutionResult(true, null); else return new ResponseExecutionResult(true, logAnnounce);
// TODO option to announce ban/kick result in the trigger channel
// ^ implementation: take result, reply to channel. don't alter BanOrKickAsync.
} }
private async Task<ResponseExecutionResult> DoDelete(SocketGuild g, SocketMessage msg) 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.Collections.Generic;
using System.Linq; using System.Linq;
namespace Kerobot.Common namespace RegexBot.Common
{ {
/// <summary> /// <summary>
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values. /// 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.Collections.Generic;
using System.Linq; using System.Linq;
namespace Kerobot.Common namespace RegexBot.Common
{ {
/// <summary> /// <summary>
/// The type of entity specified in an <see cref="EntityName"/>. /// The type of entity specified in an <see cref="EntityName"/>.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
using System; using System;
namespace Kerobot namespace RegexBot
{ {
/// <summary> /// <summary>
/// Specifies to the Kerobot module loader that the target class should be treated as a module instance. /// 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. /// the program searches for classes implementing <see cref="ModuleBase"/> that also contain this attribute.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [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 System.Threading.Tasks;
using Discord.Net; using Discord.Net;
using Discord.WebSocket; using Discord.WebSocket;
using static Kerobot.Kerobot; using static RegexBot.RegexbotClient;
namespace Kerobot.Services.CommonFunctions namespace RegexBot.Services.CommonFunctions
{ {
/// <summary> /// <summary>
/// Implements certain common actions that modules may want to perform. Using this service to perform those /// 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> /// </summary>
internal class CommonFunctionsService : Service internal class CommonFunctionsService : Service
{ {
public CommonFunctionsService(Kerobot kb) : base(kb) { } public CommonFunctionsService(RegexbotClient bot) : base(bot) { }
#region Guild member removal #region Guild member removal
/// <summary> /// <summary>
@ -33,13 +33,9 @@ namespace Kerobot.Services.CommonFunctions
SocketGuildUser utarget = guild.GetUser(target); SocketGuildUser utarget = guild.GetUser(target);
// Can't kick without obtaining user object. Quit here. // 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 // TODO notify services here as soon as we get some who will want to listen to this
#endif
// Send DM notification // Send DM notification
if (sendDmToTarget) if (sendDmToTarget)
@ -51,23 +47,19 @@ namespace Kerobot.Services.CommonFunctions
// Perform the action // Perform the action
try try
{ {
#if DEBUG
#warning "Actual kick/ban is DISABLED during debug."
#else
if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays); if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays);
else await utarget.KickAsync(logReason); else await utarget.KickAsync(logReason);
// TODO !! Insert ban reason! For kick also: Figure out a way to specify invoker. // TODO !! Insert ban reason! For kick also: Figure out a way to specify invoker.
#endif
} }
catch (HttpException ex) 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}"; 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 Discord.WebSocket;
using System.Threading.Tasks; 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; private CommonFunctionsService _svcCommonFunctions;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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