Added Ellie core project
Signed-off-by: Emotion <emotion@emotionchild.com>
This commit is contained in:
parent
a34c221952
commit
d7dd6a4817
14 changed files with 1316 additions and 6 deletions
359
src/Ellie/.editorconfig
Normal file
359
src/Ellie/.editorconfig
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
root = true
|
||||||
|
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||||
|
|
||||||
|
[obj/**]
|
||||||
|
generated_code = 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
|
||||||
|
|
||||||
|
# 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:suggestion
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||||
|
|
||||||
|
# Parentheses preferences
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
|
||||||
|
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
dotnet_style_require_accessibility_modifiers = always:error
|
||||||
|
|
||||||
|
# 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:warning
|
||||||
|
dotnet_style_prefer_compound_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
|
||||||
|
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:error
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||||
|
dotnet_style_prefer_simplified_interpolation = true
|
||||||
|
|
||||||
|
# Field preferences
|
||||||
|
dotnet_style_readonly_field = true:suggestion
|
||||||
|
|
||||||
|
# Parameter preferences
|
||||||
|
dotnet_code_quality_unused_parameters = all:warning
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_elsewhere = true
|
||||||
|
csharp_style_var_for_built_in_types = true:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
|
||||||
|
# Expression-bodied members
|
||||||
|
csharp_style_expression_bodied_accessors = true:suggestion
|
||||||
|
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||||
|
csharp_style_expression_bodied_indexers = true:suggestion
|
||||||
|
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||||
|
csharp_style_expression_bodied_local_functions = true:suggestion
|
||||||
|
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||||
|
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
|
||||||
|
csharp_style_expression_bodied_properties = true:suggestion
|
||||||
|
|
||||||
|
# Pattern matching preferences
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:error
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true:error
|
||||||
|
csharp_style_prefer_not_pattern = true:error
|
||||||
|
csharp_style_prefer_pattern_matching = true:suggestion
|
||||||
|
csharp_style_prefer_switch_expression = true
|
||||||
|
|
||||||
|
# Null-checking preferences
|
||||||
|
csharp_style_conditional_delegate_call = true:error
|
||||||
|
|
||||||
|
# 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:warning
|
||||||
|
csharp_prefer_simple_using_statement = true
|
||||||
|
|
||||||
|
# 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:error
|
||||||
|
csharp_style_inlined_variable_declaration = true:warning
|
||||||
|
csharp_style_pattern_local_over_anonymous_function = true
|
||||||
|
csharp_style_prefer_index_operator = true
|
||||||
|
csharp_style_prefer_range_operator = true
|
||||||
|
csharp_style_throw_expression = true:error
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||||
|
|
||||||
|
# 'using' directive preferences
|
||||||
|
csharp_using_directive_placement = outside_namespace:error
|
||||||
|
|
||||||
|
# Enforce file-scoped namespaces
|
||||||
|
csharp_style_namespace_declarations = file_scoped:error
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
|
||||||
|
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||||
|
csharp_style_allow_embedded_statements_on_same_line_experimental = false
|
||||||
|
|
||||||
|
#### C# Formatting Rules ####
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# 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 = one_less_than_current
|
||||||
|
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 = false
|
||||||
|
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
|
||||||
|
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
||||||
|
dotnet_naming_rule.private_readonly_field.severity = warning
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_field.symbols = private_field
|
||||||
|
dotnet_naming_rule.private_field.style = camel_case
|
||||||
|
dotnet_naming_rule.private_field.severity = warning
|
||||||
|
|
||||||
|
dotnet_naming_rule.const_fields.symbols = const_fields
|
||||||
|
dotnet_naming_rule.const_fields.style = all_upper
|
||||||
|
dotnet_naming_rule.const_fields.severity = warning
|
||||||
|
|
||||||
|
# dotnet_naming_rule.class_should_be_pascal_case.severity = error
|
||||||
|
# dotnet_naming_rule.class_should_be_pascal_case.symbols = class
|
||||||
|
# dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.struct_should_be_pascal_case.severity = error
|
||||||
|
dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct
|
||||||
|
dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
|
||||||
|
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 = error
|
||||||
|
# dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||||
|
# dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
# dotnet_naming_rule.enum_should_be_pascal_case.severity = error
|
||||||
|
# dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum
|
||||||
|
# dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
# dotnet_naming_rule.property_should_be_pascal_case.severity = error
|
||||||
|
# dotnet_naming_rule.property_should_be_pascal_case.symbols = property
|
||||||
|
# dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.method_should_be_pascal_case.severity = error
|
||||||
|
dotnet_naming_rule.method_should_be_pascal_case.symbols = method
|
||||||
|
dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
|
||||||
|
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
|
||||||
|
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
|
||||||
|
|
||||||
|
# dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
|
||||||
|
# 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
|
||||||
|
|
||||||
|
dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
|
||||||
|
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
|
||||||
|
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.const_fields.required_modifiers = const
|
||||||
|
dotnet_naming_symbols.const_fields.applicable_kinds = field
|
||||||
|
|
||||||
|
dotnet_naming_symbols.class.applicable_kinds = class
|
||||||
|
dotnet_naming_symbols.class.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.class.required_modifiers =
|
||||||
|
|
||||||
|
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.struct.applicable_kinds = struct
|
||||||
|
dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.struct.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.enum.applicable_kinds = enum
|
||||||
|
dotnet_naming_symbols.enum.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.enum.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.method.applicable_kinds = method
|
||||||
|
dotnet_naming_symbols.method.applicable_accessibilities = public
|
||||||
|
dotnet_naming_symbols.method.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.property.applicable_kinds = property
|
||||||
|
dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.property.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 =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_readonly_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_readonly_field.applicable_accessibilities = private, protected
|
||||||
|
dotnet_naming_symbols.private_readonly_field.required_modifiers = readonly
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_field.applicable_accessibilities = private, protected
|
||||||
|
dotnet_naming_symbols.private_field.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.async_method.applicable_kinds = method, local_function
|
||||||
|
dotnet_naming_symbols.async_method.applicable_accessibilities = *
|
||||||
|
dotnet_naming_symbols.async_method.required_modifiers = async
|
||||||
|
|
||||||
|
dotnet_naming_symbols.local_variable.applicable_kinds = parameter, local
|
||||||
|
dotnet_naming_symbols.local_variable.applicable_accessibilities = local
|
||||||
|
dotnet_naming_symbols.local_variable.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
|
||||||
|
dotnet_naming_style.all_upper.capitalization = all_upper
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_underscore.required_prefix = _
|
||||||
|
dotnet_naming_style.begins_with_underscore.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_underscore.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_underscore.capitalization = camel_case
|
||||||
|
|
||||||
|
dotnet_naming_style.ends_with_async.required_prefix =
|
||||||
|
# dotnet_naming_style.ends_with_async.required_suffix = Async
|
||||||
|
dotnet_naming_style.ends_with_async.word_separator =
|
||||||
|
dotnet_naming_style.ends_with_async.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.camel_case.required_prefix =
|
||||||
|
dotnet_naming_style.camel_case.required_suffix =
|
||||||
|
dotnet_naming_style.camel_case.word_separator =
|
||||||
|
dotnet_naming_style.camel_case.capitalization = camel_case
|
||||||
|
|
||||||
|
# CA1822: Mark members as static
|
||||||
|
dotnet_diagnostic.ca1822.severity = suggestion
|
||||||
|
|
||||||
|
# IDE0004: Cast is redundant
|
||||||
|
dotnet_diagnostic.ide0004.severity = warning
|
||||||
|
|
||||||
|
# IDE0058: Expression value is never used
|
||||||
|
dotnet_diagnostic.ide0058.severity = none
|
||||||
|
|
||||||
|
# # IDE0011: Add braces to 'if'/'else' statement
|
||||||
|
# dotnet_diagnostic.ide0011.severity = none
|
||||||
|
|
||||||
|
resharper_wrap_after_invocation_lpar = false
|
||||||
|
resharper_wrap_before_invocation_rpar = false
|
||||||
|
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_align_multiline_calls_chain = true
|
||||||
|
resharper_csharp_wrap_after_declaration_lpar = true
|
||||||
|
resharper_csharp_wrap_after_invocation_lpar = false
|
||||||
|
resharper_csharp_wrap_before_binary_opsign = true
|
||||||
|
resharper_csharp_wrap_before_invocation_rpar = false
|
||||||
|
resharper_csharp_wrap_parameters_style = chop_if_long
|
||||||
|
resharper_force_chop_compound_if_expression = false
|
||||||
|
resharper_keep_existing_linebreaks = true
|
||||||
|
resharper_keep_user_linebreaks = true
|
||||||
|
resharper_max_formal_parameters_on_line = 3
|
||||||
|
resharper_place_simple_embedded_statement_on_same_line = false
|
||||||
|
resharper_wrap_chained_binary_expressions = chop_if_long
|
||||||
|
resharper_wrap_chained_binary_patterns = chop_if_long
|
||||||
|
resharper_wrap_chained_method_calls = chop_if_long
|
||||||
|
resharper_wrap_object_and_collection_initializer_style = chop_always
|
||||||
|
|
||||||
|
resharper_csharp_wrap_before_first_type_parameter_constraint = true
|
||||||
|
resharper_csharp_place_type_constraints_on_same_line = false
|
||||||
|
resharper_csharp_wrap_before_extends_colon = true
|
||||||
|
resharper_csharp_place_constructor_initializer_on_same_line = false
|
||||||
|
resharper_force_attribute_style = separate
|
||||||
|
resharper_csharp_braces_for_ifelse = required_for_multiline_statement
|
||||||
|
resharper_csharp_braces_for_foreach = required_for_multiline
|
||||||
|
resharper_csharp_braces_for_while = required_for_multiline
|
||||||
|
resharper_csharp_braces_for_for = required_for_multiline
|
||||||
|
resharper_arrange_redundant_parentheses_highlighting = hint
|
||||||
|
|
||||||
|
# IDE0011: Add braces
|
||||||
|
dotnet_diagnostic.IDE0011.severity = warning
|
412
src/Ellie/Bot.cs
Normal file
412
src/Ellie/Bot.cs
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ellie.Common.Configs;
|
||||||
|
using Ellie.Common.ModuleBehaviors;
|
||||||
|
using Ellie.Db;
|
||||||
|
using Ellie.Modules.Administration;
|
||||||
|
using Ellie.Modules.Gambling;
|
||||||
|
using Ellie.Modules.Help;
|
||||||
|
using Ellie.Modules.Music;
|
||||||
|
using Ellie.Modules.EllieExpressions;
|
||||||
|
using Ellie.Modules.Patronage;
|
||||||
|
using Ellie.Modules.Permissions;
|
||||||
|
using Ellie.Modules.Searches;
|
||||||
|
using Ellie.Modules.Utility;
|
||||||
|
using Ellie.Modules.Xp;
|
||||||
|
using Ellie.Services.Database;
|
||||||
|
using Ellie.Services.Database.Models;
|
||||||
|
using Ninject;
|
||||||
|
using Ninject.Planning;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using RunMode = Discord.Commands.RunMode;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Ellie;
|
||||||
|
|
||||||
|
public class Bot : IBot
|
||||||
|
{
|
||||||
|
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||||
|
|
||||||
|
public DiscordSocketClient Client { get; set; }
|
||||||
|
public IReadOnlyCollection<GuildConfig> AllGuildConfigs { get; private set; }
|
||||||
|
|
||||||
|
private IKernel Services { get; set; }
|
||||||
|
|
||||||
|
// todo remove
|
||||||
|
public string Mention { get; private set; }
|
||||||
|
public bool IsReady { get; private set; }
|
||||||
|
public int ShardId { get; set; }
|
||||||
|
|
||||||
|
private readonly IBotCredentials _creds;
|
||||||
|
private readonly CommandService _commandService;
|
||||||
|
private readonly DbService _db;
|
||||||
|
|
||||||
|
private readonly IBotCredsProvider _credsProvider;
|
||||||
|
|
||||||
|
private readonly Assembly[] _loadedAssemblies;
|
||||||
|
// private readonly InteractionService _interactionService;
|
||||||
|
|
||||||
|
public Bot(int shardId, int? totalShards, string credPath = null)
|
||||||
|
{
|
||||||
|
if (shardId < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||||
|
|
||||||
|
ShardId = shardId;
|
||||||
|
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
||||||
|
_creds = _credsProvider.GetCreds();
|
||||||
|
|
||||||
|
_db = new EllieDbService(_credsProvider);
|
||||||
|
|
||||||
|
var messageCacheSize =
|
||||||
|
#if GLOBAL_ELLIE
|
||||||
|
0;
|
||||||
|
#else
|
||||||
|
50;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!_creds.UsePrivilegedIntents)
|
||||||
|
Log.Warning("You are not using privileged intents. Some features will not work properly");
|
||||||
|
|
||||||
|
Client = new(new()
|
||||||
|
{
|
||||||
|
MessageCacheSize = messageCacheSize,
|
||||||
|
LogLevel = LogSeverity.Warning,
|
||||||
|
ConnectionTimeout = int.MaxValue,
|
||||||
|
TotalShards = _creds.TotalShards,
|
||||||
|
ShardId = shardId,
|
||||||
|
AlwaysDownloadUsers = false,
|
||||||
|
AlwaysResolveStickers = false,
|
||||||
|
AlwaysDownloadDefaultStickers = false,
|
||||||
|
GatewayIntents = _creds.UsePrivilegedIntents
|
||||||
|
? GatewayIntents.All
|
||||||
|
: GatewayIntents.AllUnprivileged,
|
||||||
|
LogGatewayIntentWarnings = false,
|
||||||
|
FormatUsersInBidirectionalUnicode = false,
|
||||||
|
DefaultRetryMode = RetryMode.Retry502
|
||||||
|
});
|
||||||
|
|
||||||
|
_commandService = new(new()
|
||||||
|
{
|
||||||
|
CaseSensitiveCommands = false,
|
||||||
|
DefaultRunMode = RunMode.Sync,
|
||||||
|
});
|
||||||
|
|
||||||
|
// _interactionService = new(Client.Rest);
|
||||||
|
|
||||||
|
Client.Log += Client_Log;
|
||||||
|
_loadedAssemblies = new[]
|
||||||
|
{
|
||||||
|
typeof(Bot).Assembly, // bot
|
||||||
|
typeof(Creds).Assembly, // bot.common
|
||||||
|
|
||||||
|
// modules
|
||||||
|
typeof(EllieExpression).Assembly, typeof(Administration).Assembly, typeof(Gambling).Assembly,
|
||||||
|
typeof(Help).Assembly, typeof(Music).Assembly, typeof(Patronage).Assembly, typeof(Permissions).Assembly,
|
||||||
|
typeof(Searches).Assembly, typeof(Utility).Assembly, typeof(Xp).Assembly,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ulong> GetCurrentGuildIds()
|
||||||
|
=> Client.Guilds.Select(x => x.Id).ToList();
|
||||||
|
|
||||||
|
private void AddServices()
|
||||||
|
{
|
||||||
|
var startingGuildIdList = GetCurrentGuildIds();
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var bot = Client.CurrentUser;
|
||||||
|
|
||||||
|
using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
|
||||||
|
AllGuildConfigs = uow.Set<GuildConfig>().GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svcs = new StandardKernel(new NinjectSettings()
|
||||||
|
{
|
||||||
|
ThrowOnGetServiceNotFound = true,
|
||||||
|
ActivationCacheDisabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is required in order for medusa unloading to work
|
||||||
|
svcs.Components.Remove<IPlanner, Planner>();
|
||||||
|
svcs.Components.Add<IPlanner, RemovablePlanner>();
|
||||||
|
|
||||||
|
svcs.AddSingleton<IBotCredentials, IBotCredentials>(_ => _credsProvider.GetCreds());
|
||||||
|
svcs.AddSingleton<DbService, DbService>(_db);
|
||||||
|
svcs.AddSingleton<IBotCredsProvider>(_credsProvider);
|
||||||
|
svcs.AddSingleton<DiscordSocketClient>(Client);
|
||||||
|
svcs.AddSingleton<CommandService>(_commandService);
|
||||||
|
svcs.AddSingleton<Bot>(this);
|
||||||
|
svcs.AddSingleton<IBot>(this);
|
||||||
|
|
||||||
|
svcs.AddSingleton<ISeria, JsonSeria>();
|
||||||
|
svcs.AddSingleton<IConfigSeria, YamlSeria>();
|
||||||
|
svcs.AddSingleton<IMemoryCache, MemoryCache>(new MemoryCache(new MemoryCacheOptions()));
|
||||||
|
svcs.AddSingleton<IBehaviorHandler, BehaviorHandler>();
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var a in _loadedAssemblies)
|
||||||
|
{
|
||||||
|
svcs.AddConfigServices(a)
|
||||||
|
.AddConfigMigrators(a)
|
||||||
|
.AddLifetimeServices(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
svcs.AddMusic()
|
||||||
|
.AddCache(_creds)
|
||||||
|
.AddHttpClients();
|
||||||
|
|
||||||
|
if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
|
||||||
|
{
|
||||||
|
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
svcs.AddSingleton<RemoteGrpcCoordinator>();
|
||||||
|
svcs.AddSingleton<ICoordinator>(_ => svcs.GetRequiredService<RemoteGrpcCoordinator>());
|
||||||
|
svcs.AddSingleton<IReadyExecutor>(_ => svcs.GetRequiredService<RemoteGrpcCoordinator>());
|
||||||
|
}
|
||||||
|
|
||||||
|
svcs.AddSingleton<IServiceProvider>(svcs);
|
||||||
|
|
||||||
|
//initialize Services
|
||||||
|
Services = svcs;
|
||||||
|
Services.GetRequiredService<IBehaviorHandler>().Initialize();
|
||||||
|
|
||||||
|
if (Client.ShardId == 0)
|
||||||
|
ApplyConfigMigrations();
|
||||||
|
|
||||||
|
foreach (var a in _loadedAssemblies)
|
||||||
|
{
|
||||||
|
LoadTypeReaders(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyConfigMigrations()
|
||||||
|
{
|
||||||
|
// execute all migrators
|
||||||
|
var migrators = Services.GetServices<IConfigMigrator>();
|
||||||
|
foreach (var migrator in migrators)
|
||||||
|
migrator.EnsureMigrated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadTypeReaders(Assembly assembly)
|
||||||
|
{
|
||||||
|
var filteredTypes = assembly.GetTypes()
|
||||||
|
.Where(x => x.IsSubclassOf(typeof(TypeReader))
|
||||||
|
&& x.BaseType?.GetGenericArguments().Length > 0
|
||||||
|
&& !x.IsAbstract);
|
||||||
|
|
||||||
|
foreach (var ft in filteredTypes)
|
||||||
|
{
|
||||||
|
var baseType = ft.BaseType;
|
||||||
|
if (baseType is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var typeReader = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
|
||||||
|
var typeArgs = baseType.GetGenericArguments();
|
||||||
|
_commandService.AddTypeReader(typeArgs[0], typeReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoginAsync(string token)
|
||||||
|
{
|
||||||
|
var clientReady = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
async Task SetClientReady()
|
||||||
|
{
|
||||||
|
clientReady.TrySetResult(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var chan in await Client.GetDMChannelsAsync())
|
||||||
|
await chan.CloseAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//connect
|
||||||
|
Log.Information("Shard {ShardId} logging in ...", Client.ShardId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Client.Ready += SetClientReady;
|
||||||
|
|
||||||
|
await Client.LoginAsync(TokenType.Bot, token);
|
||||||
|
await Client.StartAsync();
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
LoginErrorHandler.Handle(ex);
|
||||||
|
Helpers.ReadErrorAndExit(3);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoginErrorHandler.Handle(ex);
|
||||||
|
Helpers.ReadErrorAndExit(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
await clientReady.Task.ConfigureAwait(false);
|
||||||
|
Client.Ready -= SetClientReady;
|
||||||
|
|
||||||
|
Client.JoinedGuild += Client_JoinedGuild;
|
||||||
|
Client.LeftGuild += Client_LeftGuild;
|
||||||
|
|
||||||
|
// _ = Client.SetStatusAsync(UserStatus.Online);
|
||||||
|
Log.Information("Shard {ShardId} logged in", Client.ShardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Client_LeftGuild(SocketGuild arg)
|
||||||
|
{
|
||||||
|
Log.Information("Left server: {GuildName} [{GuildId}]", arg?.Name, arg?.Id);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Client_JoinedGuild(SocketGuild arg)
|
||||||
|
{
|
||||||
|
Log.Information("Joined server: {GuildName} [{GuildId}]", arg.Name, arg.Id);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
GuildConfig gc;
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
gc = uow.GuildConfigsForId(arg.Id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JoinedGuild.Invoke(gc);
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync()
|
||||||
|
{
|
||||||
|
if (ShardId == 0)
|
||||||
|
await _db.SetupAsync();
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
await LoginAsync(_creds.Token);
|
||||||
|
|
||||||
|
Mention = Client.CurrentUser.Mention;
|
||||||
|
Log.Information("Shard {ShardId} loading services...", Client.ShardId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AddServices();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error adding services");
|
||||||
|
Helpers.ReadErrorAndExit(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
||||||
|
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||||
|
|
||||||
|
// start handling messages received in commandhandler
|
||||||
|
await commandHandler.StartHandling();
|
||||||
|
|
||||||
|
foreach (var a in _loadedAssemblies)
|
||||||
|
{
|
||||||
|
await _commandService.AddModulesAsync(a, Services);
|
||||||
|
}
|
||||||
|
|
||||||
|
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||||
|
IsReady = true;
|
||||||
|
|
||||||
|
await EnsureBotOwnershipAsync();
|
||||||
|
_ = Task.Run(ExecuteReadySubscriptions);
|
||||||
|
Log.Information("Shard {ShardId} ready", Client.ShardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask EnsureBotOwnershipAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_creds.OwnerIds.Count != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Log.Information("Initializing Owner Id...");
|
||||||
|
var info = await Client.GetApplicationInfoAsync();
|
||||||
|
_credsProvider.ModifyCredsFile(x => x.OwnerIds = new[] { info.Owner.Id });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning("Getting application info failed: {ErrorMessage}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ExecuteReadySubscriptions()
|
||||||
|
{
|
||||||
|
var readyExecutors = Services.GetServices<IReadyExecutor>();
|
||||||
|
var tasks = readyExecutors.Select(async toExec =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await toExec.OnReadyAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex,
|
||||||
|
"Failed running OnReadyAsync method on {Type} type: {Message}",
|
||||||
|
toExec.GetType().Name,
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tasks.WhenAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Client_Log(LogMessage arg)
|
||||||
|
{
|
||||||
|
if (arg.Message?.Contains("unknown dispatch", StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
|
||||||
|
{
|
||||||
|
Log.Error("""
|
||||||
|
Login failed.
|
||||||
|
|
||||||
|
*** Please enable privileged intents ***
|
||||||
|
|
||||||
|
Certain Ellie features require Discord's privileged gateway intents.
|
||||||
|
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
|
||||||
|
|
||||||
|
How to enable privileged intents:
|
||||||
|
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
|
||||||
|
2. Select your Application.
|
||||||
|
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
|
||||||
|
4. Enable all intents.
|
||||||
|
5. Restart your bot.
|
||||||
|
|
||||||
|
Read this only if your bot is in 100 or more servers:
|
||||||
|
|
||||||
|
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
|
||||||
|
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
|
||||||
|
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the ellie's features
|
||||||
|
""");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if GLOBAL_ELLIE || DEBUG
|
||||||
|
if (arg.Exception is not null)
|
||||||
|
Log.Warning(arg.Exception, "{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||||
|
else
|
||||||
|
Log.Warning("{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||||
|
#endif
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAndBlockAsync()
|
||||||
|
{
|
||||||
|
await RunAsync();
|
||||||
|
await Task.Delay(-1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,6 +97,7 @@
|
||||||
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
|
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
|
<ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
|
||||||
<ProjectReference Include="..\Ellie.Econ\Ellie.Econ.csproj" />
|
<ProjectReference Include="..\Ellie.Econ\Ellie.Econ.csproj" />
|
||||||
|
@ -129,10 +130,11 @@
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Migrations\Mysql\" />
|
<PropertyGroup Condition=" '$(Version)' == '' ">
|
||||||
<Folder Include="Migrations\Postgresql\" />
|
<VersionPrefix Condition=" '$(VersionPrefix)' == '' ">5.0.0</VersionPrefix>
|
||||||
<Folder Include="Migrations\Sqlite\" />
|
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionPrefix).$(VersionSuffix)</Version>
|
||||||
</ItemGroup>
|
<Version Condition=" '$(Version)' == '' ">$(VersionPrefix)</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
31
src/Ellie/GlobalUsings.cs
Normal file
31
src/Ellie/GlobalUsings.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// global using System.Collections.Concurrent;
|
||||||
|
global using NonBlocking;
|
||||||
|
|
||||||
|
// packages
|
||||||
|
global using Serilog;
|
||||||
|
global using Humanizer;
|
||||||
|
|
||||||
|
// ellie
|
||||||
|
global using Ellie;
|
||||||
|
global using Ellie.Services;
|
||||||
|
global using Ellise.Common; // new project
|
||||||
|
global using Ellie.Common; // old + ellie specific things
|
||||||
|
global using Ellie.Common.Attributes;
|
||||||
|
global using Ellie.Extensions;
|
||||||
|
global using Ellie.Marmalade;
|
||||||
|
|
||||||
|
// discord
|
||||||
|
global using Discord;
|
||||||
|
global using Discord.Commands;
|
||||||
|
global using Discord.Net;
|
||||||
|
global using Discord.WebSocket;
|
||||||
|
|
||||||
|
// aliases
|
||||||
|
global using GuildPerm = Discord.GuildPermission;
|
||||||
|
global using ChannelPerm = Discord.ChannelPermission;
|
||||||
|
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||||
|
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||||
|
global using TypeReaderResult = Ellie.Common.TypeReaders.TypeReaderResult;
|
||||||
|
|
||||||
|
// non-essential
|
||||||
|
global using JetBrains.Annotations;
|
18
src/Ellie/Modules/Gambling/CurrencyProvider.cs
Normal file
18
src/Ellie/Modules/Gambling/CurrencyProvider.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Ellie.Bot.Common;
|
||||||
|
using Ellie.Modules.Gambling.Services;
|
||||||
|
|
||||||
|
namespace Ellie.Modules.Gambling;
|
||||||
|
|
||||||
|
// todo do we need both currencyprovider and currencyservice
|
||||||
|
public sealed class CurrencyProvider : ICurrencyProvider, IEService
|
||||||
|
{
|
||||||
|
private readonly GamblingConfigService _cs;
|
||||||
|
|
||||||
|
public CurrencyProvider(GamblingConfigService cs)
|
||||||
|
{
|
||||||
|
_cs = cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrencySign()
|
||||||
|
=> _cs.Data.Currency.Sign;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Ellie.Modules;
|
||||||
|
|
||||||
|
public interface IMarmaladesRepositoryService
|
||||||
|
{
|
||||||
|
Task<List<ModuleItem>> GetModuleItemsAsync();
|
||||||
|
}
|
229
src/Ellie/Modules/Marmalades/Marmalade.cs
Normal file
229
src/Ellie/Modules/Marmalades/Marmalade.cs
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
using Ellie.Marmalade;
|
||||||
|
|
||||||
|
namespace Ellie.Modules;
|
||||||
|
|
||||||
|
[OwnerOnly]
|
||||||
|
public partial class Marmalade : EllieModule<IMarmaladeLoaderSevice>
|
||||||
|
{
|
||||||
|
private readonly IMarmaladesRepositoryService _repo;
|
||||||
|
|
||||||
|
public Marmalade(IMarmaladesRepositoryService repo)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task MarmaladeLoad(string? name = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
var loaded = _service.GetLoadedMarmalades()
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var unloaded = _service.GetAllMarmalades()
|
||||||
|
.Where(x => !loaded.Contains(x))
|
||||||
|
.Select(x => Format.Code(x.ToString()))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (unloaded.Length == 0)
|
||||||
|
{
|
||||||
|
await ReplyPendingLocalizedAsync(strs.no_marmalade_available);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SendPaginatedConfirmAsync(0,
|
||||||
|
page =>
|
||||||
|
{
|
||||||
|
return _eb.Create(ctx)
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.list_of_unloaded))
|
||||||
|
.WithDescription(unloaded.Skip(10 * page).Take(10).Join('\n'));
|
||||||
|
},
|
||||||
|
unloaded.Length,
|
||||||
|
10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = await _service.LoadMarmaladeAsync(name);
|
||||||
|
if (res == MarmaladeLoadResult.Success)
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.marmalade_loaded(Format.Code(name)));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var locStr = res switch
|
||||||
|
{
|
||||||
|
MarmaladeLoadResult.Empty => strs.marmalade_empty,
|
||||||
|
MarmaladeLoadResult.AlreadyLoaded => strs.marmalade_already_loaded(Format.Code(name)),
|
||||||
|
MarmaladeLoadResult.NotFound => strs.marmalade_invalid_not_found,
|
||||||
|
MarmaladeLoadResult.UnknownError => strs.error_occured,
|
||||||
|
_ => strs.error_occured
|
||||||
|
};
|
||||||
|
|
||||||
|
await ReplyErrorLocalizedAsync(locStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task MarmaladeUnload(string? name = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
var loaded = _service.GetLoadedMarmalades();
|
||||||
|
if (loaded.Count == 0)
|
||||||
|
{
|
||||||
|
await ReplyPendingLocalizedAsync(strs.no_marmalade_loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Channel.EmbedAsync(_eb.Create(ctx)
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.loaded_marmalades))
|
||||||
|
.WithDescription(loaded.Select(x => x.Name)
|
||||||
|
.Join("\n")));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = await _service.UnloadMarmaladeAsync(name);
|
||||||
|
if (res == MarmaladeUnloadResult.Success)
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.marmalade_unloaded(Format.Code(name)));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var locStr = res switch
|
||||||
|
{
|
||||||
|
MarmaladeUnloadResult.NotFound => strs.marmalade_not_loaded,
|
||||||
|
MarmaladeUnloadResult.PossiblyUnable => strs.marmalade_possibly_cant_unload,
|
||||||
|
_ => strs.error_occured
|
||||||
|
};
|
||||||
|
|
||||||
|
await ReplyErrorLocalizedAsync(locStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task MarmaladeList()
|
||||||
|
{
|
||||||
|
var all = _service.GetAllMarmalades();
|
||||||
|
|
||||||
|
if (all.Count == 0)
|
||||||
|
{
|
||||||
|
await ReplyPendingLocalizedAsync(strs.no_marmalade_available);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loaded = _service.GetLoadedMarmalades()
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var output = all
|
||||||
|
.Select(m =>
|
||||||
|
{
|
||||||
|
var emoji = loaded.Contains(m) ? "`✅`" : "`🔴`";
|
||||||
|
return $"{emoji} `{m}`";
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
|
||||||
|
await ctx.SendPaginatedConfirmAsync(0,
|
||||||
|
page => _eb.Create(ctx)
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.list_of_marmalades))
|
||||||
|
.WithDescription(output.Skip(page * 10).Take(10).Join('\n')),
|
||||||
|
output.Length,
|
||||||
|
10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task MarmaladeInfo(string? name = null)
|
||||||
|
{
|
||||||
|
var marmalades = _service.GetLoadedMarmalades();
|
||||||
|
|
||||||
|
if (name is not null)
|
||||||
|
{
|
||||||
|
var found = marmalades.FirstOrDefault(x => string.Equals(x.Name,
|
||||||
|
name,
|
||||||
|
StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (found is null)
|
||||||
|
{
|
||||||
|
await ReplyErrorLocalizedAsync(strs.marmalade_name_not_found);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdCount = found.Canaries.Sum(x => x.Commands.Count);
|
||||||
|
var cmdNames = found.Canaries
|
||||||
|
.SelectMany(x => Format.Code(string.IsNullOrWhiteSpace(x.Prefix)
|
||||||
|
? x.Name
|
||||||
|
: $"{x.Prefix} {x.Name}"))
|
||||||
|
.Join("\n");
|
||||||
|
|
||||||
|
var eb = _eb.Create(ctx)
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(GetText(strs.marmalade_info))
|
||||||
|
.WithTitle(found.Name)
|
||||||
|
.WithDescription(found.Description)
|
||||||
|
.AddField(GetText(strs.sneks_count(found.Canaries.Count)),
|
||||||
|
found.Canaries.Count == 0
|
||||||
|
? "-"
|
||||||
|
: found.Canaries.Select(x => x.Name).Join('\n'),
|
||||||
|
true)
|
||||||
|
.AddField(GetText(strs.commands_count(cmdCount)),
|
||||||
|
string.IsNullOrWhiteSpace(cmdNames)
|
||||||
|
? "-"
|
||||||
|
: cmdNames,
|
||||||
|
true);
|
||||||
|
|
||||||
|
await ctx.Channel.EmbedAsync(eb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marmalades.Count == 0)
|
||||||
|
{
|
||||||
|
await ReplyPendingLocalizedAsync(strs.no_marmalade_loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SendPaginatedConfirmAsync(0,
|
||||||
|
page =>
|
||||||
|
{
|
||||||
|
var eb = _eb.Create(ctx)
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
foreach (var marmalade in marmalades.Skip(page * 9).Take(9))
|
||||||
|
{
|
||||||
|
eb.AddField(marmalade.Name,
|
||||||
|
$"""
|
||||||
|
`Canaries:` {marmalade.Canaries.Count}
|
||||||
|
`Commands:` {marmalade.Canaries.Sum(x => x.Commands.Count)}
|
||||||
|
--
|
||||||
|
{marmalade.Description}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
}, marmalades.Count, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task MarmaladeSearch()
|
||||||
|
{
|
||||||
|
var eb = _eb.Create()
|
||||||
|
.WithTitle(GetText(strs.list_of_marmalades))
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
foreach (var item in await _repo.GetModuleItemsAsync())
|
||||||
|
{
|
||||||
|
eb.AddField(item.Name, $"""
|
||||||
|
{item.Description}
|
||||||
|
`{item.Command}`
|
||||||
|
""", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Channel.EmbedAsync(eb);
|
||||||
|
}
|
||||||
|
}
|
8
src/Ellie/Modules/Marmalades/MarmaladeItem.cs
Normal file
8
src/Ellie/Modules/Marmalades/MarmaladeItem.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Ellie.Modules;
|
||||||
|
|
||||||
|
public sealed class ModuleItem
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
public required string Command { get; init; }
|
||||||
|
}
|
22
src/Ellie/Modules/Marmalades/MarmaladesRepositoryService.cs
Normal file
22
src/Ellie/Modules/Marmalades/MarmaladesRepositoryService.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Ellie.Modules;
|
||||||
|
|
||||||
|
public class MarmaladesRepositoryService : IMarmaladesRepositoryService, IEService
|
||||||
|
{
|
||||||
|
public async Task<List<ModuleItem>> GetModuleItemsAsync()
|
||||||
|
{
|
||||||
|
// Simulate retrieving data from a database or API
|
||||||
|
await Task.Delay(100);
|
||||||
|
return new List<ModuleItem>
|
||||||
|
{
|
||||||
|
new ModuleItem { Name = "RSS Reader", Description = "Keep up to date with your favorite websites", Command = ".mainstall rss" },
|
||||||
|
new ModuleItem { Name = "Password Manager", Description = "Safely store and manage all your passwords", Command = ".mainstall passwordmanager" },
|
||||||
|
new ModuleItem { Name = "Browser Extension", Description = "Enhance your browsing experience with useful tools", Command = ".mainstall browserextension" },
|
||||||
|
new ModuleItem { Name = "Video Downloader", Description = "Download videos from popular websites", Command = ".mainstall videodownloader" },
|
||||||
|
new ModuleItem { Name = "Virtual Private Network", Description = "Securely browse the web and protect your privacy", Command = ".mainstall vpn" },
|
||||||
|
new ModuleItem { Name = "Ad Blocker", Description = "Block annoying ads and improve page load times", Command = ".mainstall adblocker" },
|
||||||
|
new ModuleItem { Name = "Cloud Storage", Description = "Store and share your files online", Command = ".mainstall cloudstorage" },
|
||||||
|
new ModuleItem { Name = "Social Media Manager", Description = "Manage all your social media accounts in one place", Command = ".mainstall socialmediamanager" },
|
||||||
|
new ModuleItem { Name = "Code Editor", Description = "Write and edit code online", Command = ".mainstall codeeditor" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
80
src/Ellie/PermissionChecker.cs
Normal file
80
src/Ellie/PermissionChecker.cs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
using Ellie.Bot.Common;
|
||||||
|
using Ellie.Modules.Permissions.Common;
|
||||||
|
using Ellie.Modules.Permissions.Services;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Ellie;
|
||||||
|
|
||||||
|
public sealed class PermissionChecker : IPermissionChecker, IEService
|
||||||
|
{
|
||||||
|
private readonly PermissionService _perms;
|
||||||
|
private readonly GlobalPermissionService _gperm;
|
||||||
|
private readonly CmdCdService _cmdCds;
|
||||||
|
|
||||||
|
public PermissionChecker(PermissionService perms, GlobalPermissionService gperm, CmdCdService cmdCds)
|
||||||
|
{
|
||||||
|
_perms = perms;
|
||||||
|
_gperm = gperm;
|
||||||
|
_cmdCds = cmdCds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OneOf<Success, Error<LocStr>>> CheckAsync(
|
||||||
|
IGuild guild,
|
||||||
|
IMessageChannel channel,
|
||||||
|
IUser author,
|
||||||
|
string module,
|
||||||
|
string? cmd)
|
||||||
|
{
|
||||||
|
module = module.ToLowerInvariant();
|
||||||
|
cmd = cmd?.ToLowerInvariant();
|
||||||
|
// todo add proper string
|
||||||
|
if (cmd is not null && await _cmdCds.TryBlock(guild, author, cmd))
|
||||||
|
return new Error<LocStr>(new());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_gperm.BlockedModules.Contains(module))
|
||||||
|
{
|
||||||
|
Log.Information("u:{UserId} tried to use module {Module} which is globally disabled.",
|
||||||
|
author.Id,
|
||||||
|
module
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guild is SocketGuild sg)
|
||||||
|
{
|
||||||
|
var pc = _perms.GetCacheFor(guild.Id);
|
||||||
|
if (!pc.Permissions.CheckPermissions(author, channel, cmd, "ACTUALEXPRESSIONS", out var index))
|
||||||
|
{
|
||||||
|
if (pc.Verbose)
|
||||||
|
{
|
||||||
|
// todo fix
|
||||||
|
// var permissionMessage = strs.perm_prevent(index + 1,
|
||||||
|
// Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg)));
|
||||||
|
//
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// await msg.Channel.SendErrorAsync(_eb, permissionMessage);
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Log.Information("{PermissionMessage}", permissionMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo add proper string
|
||||||
|
return new Error<LocStr>(new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Success();
|
||||||
|
}
|
||||||
|
}
|
28
src/Ellie/Program.cs
Normal file
28
src/Ellie/Program.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
var pid = Environment.ProcessId;
|
||||||
|
|
||||||
|
var shardId = 0;
|
||||||
|
int? totalShards = null; // 0 to read from creds.yml
|
||||||
|
if (args.Length > 0 && args[0] != "run")
|
||||||
|
{
|
||||||
|
if (!int.TryParse(args[0], out shardId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Invalid first argument (shard id): {0}", args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(args[1], out var shardCount))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Invalid second argument (total shards): {0}", args[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalShards = shardCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSetup.SetupLogger(shardId);
|
||||||
|
Log.Information("Pid: {ProcessId}", pid);
|
||||||
|
|
||||||
|
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("Ellie__creds")).RunAndBlockAsync();
|
|
@ -133,7 +133,7 @@ public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
scan.From(a)
|
scan.From(a)
|
||||||
.SelectAllClasses()
|
.SelectAllClasses()
|
||||||
.Where(c => (c.IsAssignableTo(typeof(INService))
|
.Where(c => (c.IsAssignableTo(typeof(IEService))
|
||||||
|| c.IsAssignableTo(typeof(IExecOnMessage))
|
|| c.IsAssignableTo(typeof(IExecOnMessage))
|
||||||
|| c.IsAssignableTo(typeof(IInputTransformer))
|
|| c.IsAssignableTo(typeof(IInputTransformer))
|
||||||
|| c.IsAssignableTo(typeof(IExecPreCommand))
|
|| c.IsAssignableTo(typeof(IExecPreCommand))
|
||||||
|
|
115
src/Ellie/creds_example.yml
Normal file
115
src/Ellie/creds_example.yml
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# DO NOT CHANGE
|
||||||
|
version: 7
|
||||||
|
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
||||||
|
token: ''
|
||||||
|
# List of Ids of the users who have bot owner permissions
|
||||||
|
# **DO NOT ADD PEOPLE YOU DON'T TRUST**
|
||||||
|
ownerIds: []
|
||||||
|
# Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted
|
||||||
|
usePrivilegedIntents: true
|
||||||
|
# The number of shards that the bot will be running on.
|
||||||
|
# Leave at 1 if you don't know what you're doing.
|
||||||
|
#
|
||||||
|
# note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
||||||
|
# Also, in that case you should be using Ellie.Coordinator to start the bot, and it will correctly override this value.
|
||||||
|
totalShards: 1
|
||||||
|
# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||||
|
# Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||||
|
# Used only for Youtube Data Api (at the moment).
|
||||||
|
googleApiKey: ''
|
||||||
|
# Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
||||||
|
# Enable SafeSearch
|
||||||
|
# Remove all Sites to Search
|
||||||
|
# Enable Search the entire web
|
||||||
|
# Copy the 'Search Engine ID' to the SearchId field
|
||||||
|
#
|
||||||
|
# Do all steps again but enable image search for the ImageSearchId
|
||||||
|
google:
|
||||||
|
searchId:
|
||||||
|
imageSearchId:
|
||||||
|
# Settings for voting system for discordbots. Meant for use on global Nadeko.
|
||||||
|
votes:
|
||||||
|
# top.gg votes service url
|
||||||
|
# This is the url of your instance of the Ellie.Votes api
|
||||||
|
# Example: https://votes.my.cool.bot.com
|
||||||
|
topggServiceUrl: ''
|
||||||
|
# Authorization header value sent to the TopGG service url with each request
|
||||||
|
# This should be equivalent to the TopggKey in your Ellie.Votes api appsettings.json file
|
||||||
|
topggKey: ''
|
||||||
|
# discords.com votes service url
|
||||||
|
# This is the url of your instance of the Ellie.Votes api
|
||||||
|
# Example: https://votes.my.cool.bot.com
|
||||||
|
discordsServiceUrl: ''
|
||||||
|
# Authorization header value sent to the Discords service url with each request
|
||||||
|
# This should be equivalent to the DiscordsKey in your Ellie.Votes api appsettings.json file
|
||||||
|
discordsKey: ''
|
||||||
|
# Patreon auto reward system settings.
|
||||||
|
# go to https://www.patreon.com/portal -> my clients -> create client
|
||||||
|
patreon:
|
||||||
|
clientId:
|
||||||
|
accessToken: ''
|
||||||
|
refreshToken: ''
|
||||||
|
clientSecret: ''
|
||||||
|
# Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)
|
||||||
|
campaignId: ''
|
||||||
|
# Api key for sending stats to DiscordBotList.
|
||||||
|
botListToken: ''
|
||||||
|
# Official cleverbot api key.
|
||||||
|
cleverbotApiKey: ''
|
||||||
|
# Official GPT-3 api key.
|
||||||
|
gpt3ApiKey: ''
|
||||||
|
# Which cache implementation should bot use.
|
||||||
|
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||||
|
# 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
||||||
|
botCache: Memory
|
||||||
|
# Redis connection string. Don't change if you don't know what you're doing.
|
||||||
|
# Only used if botCache is set to 'redis'
|
||||||
|
redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=
|
||||||
|
# Database options. Don't change if you don't know what you're doing. Leave null for default values
|
||||||
|
db:
|
||||||
|
# Database type. "sqlite", "mysql" and "postgresql" are supported.
|
||||||
|
# Default is "sqlite"
|
||||||
|
type: sqlite
|
||||||
|
# Database connection string.
|
||||||
|
# You MUST change this if you're not using "sqlite" type.
|
||||||
|
# Default is "Data Source=data/Ellie.db"
|
||||||
|
# Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
|
||||||
|
# Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
|
||||||
|
connectionString: Data Source=data/Ellie.db
|
||||||
|
# Address and port of the coordinator endpoint. Leave empty for default.
|
||||||
|
# Change only if you've changed the coordinator address or port.
|
||||||
|
coordinatorUrl: http://localhost:3442
|
||||||
|
# Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)
|
||||||
|
rapidApiKey:
|
||||||
|
# https://locationiq.com api key (register and you will receive the token in the email).
|
||||||
|
# Used only for .time command.
|
||||||
|
locationIqApiKey:
|
||||||
|
# https://timezonedb.com api key (register and you will receive the token in the email).
|
||||||
|
# Used only for .time command
|
||||||
|
timezoneDbApiKey:
|
||||||
|
# https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
||||||
|
# Used for cryptocurrency related commands.
|
||||||
|
coinmarketcapApiKey:
|
||||||
|
# Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api
|
||||||
|
osuApiKey:
|
||||||
|
# Optional Trovo client id.
|
||||||
|
# You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
||||||
|
trovoClientId:
|
||||||
|
# Obtain by creating an application at https://dev.twitch.tv/console/apps
|
||||||
|
twitchClientId:
|
||||||
|
# Obtain by creating an application at https://dev.twitch.tv/console/apps
|
||||||
|
twitchClientSecret:
|
||||||
|
# Command and args which will be used to restart the bot.
|
||||||
|
# Only used if bot is executed directly (NOT through the coordinator)
|
||||||
|
# placeholders:
|
||||||
|
# {0} -> shard id
|
||||||
|
# {1} -> total shards
|
||||||
|
# Linux default
|
||||||
|
# cmd: dotnet
|
||||||
|
# args: "Ellie.dll -- {0}"
|
||||||
|
# Windows default
|
||||||
|
# cmd: Ellie.exe
|
||||||
|
# args: "{0}"
|
||||||
|
restartCommand:
|
||||||
|
cmd:
|
||||||
|
args:
|
BIN
src/Ellie/ellie_icon.ico
Normal file
BIN
src/Ellie/ellie_icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Loading…
Reference in a new issue