Added Ellie core project

Signed-off-by: Emotion <emotion@emotionchild.com>
This commit is contained in:
Emotion 2023-08-25 15:10:18 +12:00
parent a34c221952
commit d7dd6a4817
No known key found for this signature in database
GPG key ID: D7D3E4C27A98C37B
14 changed files with 1316 additions and 6 deletions

359
src/Ellie/.editorconfig Normal file
View 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
View 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);
}
}

View file

@ -97,6 +97,7 @@
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
<ProjectReference Include="..\Ellie.Econ\Ellie.Econ.csproj" />
@ -129,10 +130,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\Mysql\" />
<Folder Include="Migrations\Postgresql\" />
<Folder Include="Migrations\Sqlite\" />
</ItemGroup>
<PropertyGroup Condition=" '$(Version)' == '' ">
<VersionPrefix Condition=" '$(VersionPrefix)' == '' ">5.0.0</VersionPrefix>
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionPrefix).$(VersionSuffix)</Version>
<Version Condition=" '$(Version)' == '' ">$(VersionPrefix)</Version>
</PropertyGroup>
</Project>

31
src/Ellie/GlobalUsings.cs Normal file
View 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;

View 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;
}

View file

@ -0,0 +1,6 @@
namespace Ellie.Modules;
public interface IMarmaladesRepositoryService
{
Task<List<ModuleItem>> GetModuleItemsAsync();
}

View 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);
}
}

View 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; }
}

View 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" }
};
}
}

View 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
View 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();

View file

@ -133,7 +133,7 @@ public static class ServiceCollectionExtensions
{
scan.From(a)
.SelectAllClasses()
.Where(c => (c.IsAssignableTo(typeof(INService))
.Where(c => (c.IsAssignableTo(typeof(IEService))
|| c.IsAssignableTo(typeof(IExecOnMessage))
|| c.IsAssignableTo(typeof(IInputTransformer))
|| c.IsAssignableTo(typeof(IExecPreCommand))

115
src/Ellie/creds_example.yml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB