mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Updated editorconfig to (mostly?) require braces around if/else statements, and applied the new formatting rules
This commit is contained in:
		@@ -97,10 +97,10 @@ 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
 | 
			
		||||
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:suggestion
 | 
			
		||||
csharp_prefer_braces = when_multiline:error
 | 
			
		||||
csharp_prefer_simple_using_statement = true
 | 
			
		||||
 | 
			
		||||
# Expression-level preferences
 | 
			
		||||
@@ -124,7 +124,7 @@ 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 = true
 | 
			
		||||
csharp_style_allow_embedded_statements_on_same_line_experimental = false
 | 
			
		||||
 | 
			
		||||
#### C# Formatting Rules ####
 | 
			
		||||
 | 
			
		||||
@@ -171,7 +171,7 @@ csharp_space_between_square_brackets = false
 | 
			
		||||
 | 
			
		||||
# Wrapping preferences
 | 
			
		||||
csharp_preserve_single_line_blocks = true
 | 
			
		||||
csharp_preserve_single_line_statements = true
 | 
			
		||||
csharp_preserve_single_line_statements = false
 | 
			
		||||
 | 
			
		||||
#### Naming styles ####
 | 
			
		||||
 | 
			
		||||
@@ -236,35 +236,35 @@ 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.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.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.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.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.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.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.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.non_field_members.required_modifiers =
 | 
			
		||||
 | 
			
		||||
dotnet_naming_symbols.private_readonly_field.applicable_kinds = field
 | 
			
		||||
dotnet_naming_symbols.private_readonly_field.applicable_accessibilities = private, protected
 | 
			
		||||
@@ -272,7 +272,7 @@ 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.private_field.required_modifiers =
 | 
			
		||||
 | 
			
		||||
dotnet_naming_symbols.async_method.applicable_kinds = method, local_function
 | 
			
		||||
dotnet_naming_symbols.async_method.applicable_accessibilities = *
 | 
			
		||||
@@ -280,36 +280,36 @@ 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 = 
 | 
			
		||||
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.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.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.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_prefix =
 | 
			
		||||
# dotnet_naming_style.ends_with_async.required_suffix = Async
 | 
			
		||||
dotnet_naming_style.ends_with_async.word_separator = 
 | 
			
		||||
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.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
 | 
			
		||||
@@ -321,8 +321,8 @@ 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
 | 
			
		||||
# # IDE0011: Add braces to 'if'/'else' statement
 | 
			
		||||
# dotnet_diagnostic.ide0011.severity = none
 | 
			
		||||
 | 
			
		||||
resharper_wrap_after_invocation_lpar = false
 | 
			
		||||
resharper_wrap_before_invocation_rpar = false
 | 
			
		||||
@@ -349,5 +349,8 @@ 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_braces_for_ifelse = required_for_multiline
 | 
			
		||||
resharper_csharp_braces_for_ifelse = required_for_complex
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,11 @@ public sealed class Bot
 | 
			
		||||
    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 InteractionService _interactionService;
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +43,8 @@ public sealed class Bot
 | 
			
		||||
 | 
			
		||||
        _db = new(_creds);
 | 
			
		||||
 | 
			
		||||
        if (shardId == 0) _db.Setup();
 | 
			
		||||
        if (shardId == 0)
 | 
			
		||||
            _db.Setup();
 | 
			
		||||
 | 
			
		||||
        Client = new(new()
 | 
			
		||||
        {
 | 
			
		||||
@@ -55,10 +57,14 @@ public sealed class Bot
 | 
			
		||||
            AlwaysResolveStickers = false,
 | 
			
		||||
            AlwaysDownloadDefaultStickers = false,
 | 
			
		||||
            GatewayIntents = GatewayIntents.All,
 | 
			
		||||
            LogGatewayIntentWarnings = false,
 | 
			
		||||
            LogGatewayIntentWarnings = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        _commandService = new(new() { CaseSensitiveCommands = false, DefaultRunMode = RunMode.Sync });
 | 
			
		||||
        _commandService = new(new()
 | 
			
		||||
        {
 | 
			
		||||
            CaseSensitiveCommands = false,
 | 
			
		||||
            DefaultRunMode = RunMode.Sync
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // _interactionService = new(Client.Rest);
 | 
			
		||||
 | 
			
		||||
@@ -100,7 +106,7 @@ public sealed class Bot
 | 
			
		||||
                                          .AddMemoryCache()
 | 
			
		||||
                                          // music
 | 
			
		||||
                                          .AddMusic();
 | 
			
		||||
                                          // admin
 | 
			
		||||
        // admin
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
        svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
 | 
			
		||||
#else
 | 
			
		||||
@@ -109,7 +115,10 @@ public sealed class Bot
 | 
			
		||||
 | 
			
		||||
        svcs.AddHttpClient();
 | 
			
		||||
        svcs.AddHttpClient("memelist")
 | 
			
		||||
            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false });
 | 
			
		||||
            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
 | 
			
		||||
            {
 | 
			
		||||
                AllowAutoRedirect = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
 | 
			
		||||
            svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
 | 
			
		||||
@@ -147,7 +156,8 @@ public sealed class Bot
 | 
			
		||||
        var exec = Services.GetRequiredService<IBehaviourExecutor>();
 | 
			
		||||
        exec.Initialize();
 | 
			
		||||
 | 
			
		||||
        if (Client.ShardId == 0) ApplyConfigMigrations();
 | 
			
		||||
        if (Client.ShardId == 0)
 | 
			
		||||
            ApplyConfigMigrations();
 | 
			
		||||
 | 
			
		||||
        _ = LoadTypeReaders(typeof(Bot).Assembly);
 | 
			
		||||
 | 
			
		||||
@@ -201,12 +211,13 @@ public sealed class Bot
 | 
			
		||||
 | 
			
		||||
        Task SetClientReady()
 | 
			
		||||
        {
 | 
			
		||||
            _= Task.Run(async () =>
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                clientReady.TrySetResult(true);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var chan in await Client.GetDMChannelsAsync()) await chan.CloseAsync();
 | 
			
		||||
                    foreach (var chan in await Client.GetDMChannelsAsync())
 | 
			
		||||
                        await chan.CloseAsync();
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
@@ -253,7 +264,7 @@ public sealed class Bot
 | 
			
		||||
    private Task Client_JoinedGuild(SocketGuild arg)
 | 
			
		||||
    {
 | 
			
		||||
        Log.Information("Joined server: {GuildName} [{GuildId}]", arg.Name, arg.Id);
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            GuildConfig gc;
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ public static class CommandNameLoadHelper
 | 
			
		||||
{
 | 
			
		||||
    private static readonly IDeserializer _deserializer = new Deserializer();
 | 
			
		||||
 | 
			
		||||
    private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases 
 | 
			
		||||
    private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
 | 
			
		||||
        = new(() => LoadAliases());
 | 
			
		||||
 | 
			
		||||
    public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
                AcquireAllLocks(ref acquiredLocks);
 | 
			
		||||
 | 
			
		||||
                for (var i = 0; i < tables.CountPerLock.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (tables.CountPerLock[i] != 0)
 | 
			
		||||
                        return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
@@ -84,7 +86,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            {
 | 
			
		||||
                AcquireAllLocks(ref acquiredLocks);
 | 
			
		||||
 | 
			
		||||
                for (var i = 0; i < tables.CountPerLock.Length; i++) count += tables.CountPerLock[i];
 | 
			
		||||
                for (var i = 0; i < tables.CountPerLock.Length; i++)
 | 
			
		||||
                    count += tables.CountPerLock[i];
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
@@ -209,7 +212,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
    public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
 | 
			
		||||
        : this(comparer)
 | 
			
		||||
    {
 | 
			
		||||
        if (collection is null) throw new ArgumentNullException(nameof(collection));
 | 
			
		||||
        if (collection is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(collection));
 | 
			
		||||
 | 
			
		||||
        InitializeFromCollection(collection);
 | 
			
		||||
    }
 | 
			
		||||
@@ -244,8 +248,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
    public ConcurrentHashSet(int concurrencyLevel, IEnumerable<T> collection, IEqualityComparer<T> comparer)
 | 
			
		||||
        : this(concurrencyLevel, DEFAULT_CAPACITY, false, comparer)
 | 
			
		||||
    {
 | 
			
		||||
        if (collection is null) throw new ArgumentNullException(nameof(collection));
 | 
			
		||||
        if (comparer is null) throw new ArgumentNullException(nameof(comparer));
 | 
			
		||||
        if (collection is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(collection));
 | 
			
		||||
        if (comparer is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(comparer));
 | 
			
		||||
 | 
			
		||||
        InitializeFromCollection(collection);
 | 
			
		||||
    }
 | 
			
		||||
@@ -285,15 +291,19 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
        bool growLockArray,
 | 
			
		||||
        IEqualityComparer<T> comparer)
 | 
			
		||||
    {
 | 
			
		||||
        if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel));
 | 
			
		||||
        if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity));
 | 
			
		||||
        if (concurrencyLevel < 1)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(concurrencyLevel));
 | 
			
		||||
        if (capacity < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(capacity));
 | 
			
		||||
 | 
			
		||||
        // The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard
 | 
			
		||||
        // any buckets.
 | 
			
		||||
        if (capacity < concurrencyLevel) capacity = concurrencyLevel;
 | 
			
		||||
        if (capacity < concurrencyLevel)
 | 
			
		||||
            capacity = concurrencyLevel;
 | 
			
		||||
 | 
			
		||||
        var locks = new object[concurrencyLevel];
 | 
			
		||||
        for (var i = 0; i < locks.Length; i++) locks[i] = new();
 | 
			
		||||
        for (var i = 0; i < locks.Length; i++)
 | 
			
		||||
            locks[i] = new();
 | 
			
		||||
 | 
			
		||||
        var countPerLock = new int[locks.Length];
 | 
			
		||||
        var buckets = new Node[capacity];
 | 
			
		||||
@@ -335,7 +345,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
        var hashcode = _comparer.GetHashCode(item!);
 | 
			
		||||
 | 
			
		||||
        // We must capture the _buckets field in a local variable. It is set to a new table on each table resize.
 | 
			
		||||
        var localTables = this.tables;
 | 
			
		||||
        var localTables = tables;
 | 
			
		||||
 | 
			
		||||
        var bucketNo = GetBucket(hashcode, localTables.Buckets.Length);
 | 
			
		||||
 | 
			
		||||
@@ -359,8 +369,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
    void ICollection<T>.CopyTo(T[] array, int arrayIndex)
 | 
			
		||||
    {
 | 
			
		||||
        if (array is null) throw new ArgumentNullException(nameof(array));
 | 
			
		||||
        if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex));
 | 
			
		||||
        if (array is null)
 | 
			
		||||
            throw new ArgumentNullException(nameof(array));
 | 
			
		||||
        if (arrayIndex < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(arrayIndex));
 | 
			
		||||
 | 
			
		||||
        var locksAcquired = 0;
 | 
			
		||||
        try
 | 
			
		||||
@@ -369,7 +381,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
            var count = 0;
 | 
			
		||||
 | 
			
		||||
            for (var i = 0; i < tables.Locks.Length && count >= 0; i++) count += tables.CountPerLock[i];
 | 
			
		||||
            for (var i = 0; i < tables.Locks.Length && count >= 0; i++)
 | 
			
		||||
                count += tables.CountPerLock[i];
 | 
			
		||||
 | 
			
		||||
            if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow
 | 
			
		||||
                throw new ArgumentException(
 | 
			
		||||
@@ -444,20 +457,26 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
        var hashcode = _comparer.GetHashCode(item);
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            var localTables = this.tables;
 | 
			
		||||
            var localTables = tables;
 | 
			
		||||
 | 
			
		||||
            GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
 | 
			
		||||
            GetBucketAndLockNo(hashcode,
 | 
			
		||||
                out var bucketNo,
 | 
			
		||||
                out var lockNo,
 | 
			
		||||
                localTables.Buckets.Length,
 | 
			
		||||
                localTables.Locks.Length);
 | 
			
		||||
 | 
			
		||||
            lock (localTables.Locks[lockNo])
 | 
			
		||||
            {
 | 
			
		||||
                // If the table just got resized, we may not be holding the right lock, and must retry.
 | 
			
		||||
                // This should be a rare occurrence.
 | 
			
		||||
                if (localTables != this.tables) continue;
 | 
			
		||||
                if (localTables != tables)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                Node previous = null;
 | 
			
		||||
                for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
 | 
			
		||||
                {
 | 
			
		||||
                    Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
 | 
			
		||||
                    Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
 | 
			
		||||
                                 || previous!.Next == current);
 | 
			
		||||
 | 
			
		||||
                    if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
 | 
			
		||||
                    {
 | 
			
		||||
@@ -480,17 +499,23 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
    private void InitializeFromCollection(IEnumerable<T> collection)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var item in collection) AddInternal(item, _comparer.GetHashCode(item), false);
 | 
			
		||||
        foreach (var item in collection)
 | 
			
		||||
            AddInternal(item, _comparer.GetHashCode(item), false);
 | 
			
		||||
 | 
			
		||||
        if (budget == 0) budget = tables.Buckets.Length / tables.Locks.Length;
 | 
			
		||||
        if (budget == 0)
 | 
			
		||||
            budget = tables.Buckets.Length / tables.Locks.Length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool AddInternal(T item, int hashcode, bool acquireLock)
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            var localTables = this.tables;
 | 
			
		||||
            GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
 | 
			
		||||
            var localTables = tables;
 | 
			
		||||
            GetBucketAndLockNo(hashcode,
 | 
			
		||||
                out var bucketNo,
 | 
			
		||||
                out var lockNo,
 | 
			
		||||
                localTables.Buckets.Length,
 | 
			
		||||
                localTables.Locks.Length);
 | 
			
		||||
 | 
			
		||||
            var resizeDesired = false;
 | 
			
		||||
            var lockTaken = false;
 | 
			
		||||
@@ -501,13 +526,15 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
                // If the table just got resized, we may not be holding the right lock, and must retry.
 | 
			
		||||
                // This should be a rare occurrence.
 | 
			
		||||
                if (localTables != this.tables) continue;
 | 
			
		||||
                if (localTables != tables)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                // Try to find this item in the bucket
 | 
			
		||||
                Node previous = null;
 | 
			
		||||
                for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
 | 
			
		||||
                {
 | 
			
		||||
                    Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
 | 
			
		||||
                    Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
 | 
			
		||||
                                 || previous!.Next == current);
 | 
			
		||||
                    if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
 | 
			
		||||
                        return false;
 | 
			
		||||
 | 
			
		||||
@@ -526,7 +553,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
                // It is also possible that GrowTable will increase the budget but won't resize the bucket table.
 | 
			
		||||
                // That happens if the bucket table is found to be poorly utilized due to a bad hash function.
 | 
			
		||||
                //
 | 
			
		||||
                if (localTables.CountPerLock[lockNo] > budget) resizeDesired = true;
 | 
			
		||||
                if (localTables.CountPerLock[lockNo] > budget)
 | 
			
		||||
                    resizeDesired = true;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
@@ -542,7 +570,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
 | 
			
		||||
            //   and then verify that the table we passed to it as the argument is still the current table.
 | 
			
		||||
            //
 | 
			
		||||
            if (resizeDesired) GrowTable(localTables);
 | 
			
		||||
            if (resizeDesired)
 | 
			
		||||
                GrowTable(localTables);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
@@ -579,7 +608,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            AcquireLocks(0, 1, ref locksAcquired);
 | 
			
		||||
 | 
			
		||||
            // Make sure nobody resized the table while we were waiting for lock 0:
 | 
			
		||||
            if (localTables != this.tables)
 | 
			
		||||
            if (localTables != tables)
 | 
			
		||||
                // We assume that since the table reference is different, it was already resized (or the budget
 | 
			
		||||
                // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
 | 
			
		||||
                // we will have to revisit this logic.
 | 
			
		||||
@@ -587,7 +616,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
            // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
 | 
			
		||||
            long approxCount = 0;
 | 
			
		||||
            for (var i = 0; i < localTables.CountPerLock.Length; i++) approxCount += localTables.CountPerLock[i];
 | 
			
		||||
            for (var i = 0; i < localTables.CountPerLock.Length; i++)
 | 
			
		||||
                approxCount += localTables.CountPerLock[i];
 | 
			
		||||
 | 
			
		||||
            //
 | 
			
		||||
            // If the bucket array is too empty, double the budget instead of resizing the table
 | 
			
		||||
@@ -595,7 +625,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            if (approxCount < localTables.Buckets.Length / 4)
 | 
			
		||||
            {
 | 
			
		||||
                budget = 2 * budget;
 | 
			
		||||
                if (budget < 0) budget = int.MaxValue;
 | 
			
		||||
                if (budget < 0)
 | 
			
		||||
                    budget = int.MaxValue;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -613,11 +644,13 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
 | 
			
		||||
                    // Now, we only need to check odd integers, and find the first that is not divisible
 | 
			
		||||
                    // by 3, 5 or 7.
 | 
			
		||||
                    while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) newLength += 2;
 | 
			
		||||
                    while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0)
 | 
			
		||||
                        newLength += 2;
 | 
			
		||||
 | 
			
		||||
                    Debug.Assert(newLength % 2 != 0);
 | 
			
		||||
 | 
			
		||||
                    if (newLength > maxArrayLength) maximizeTableSize = true;
 | 
			
		||||
                    if (newLength > maxArrayLength)
 | 
			
		||||
                        maximizeTableSize = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (OverflowException)
 | 
			
		||||
@@ -647,7 +680,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            {
 | 
			
		||||
                newLocks = new object[localTables.Locks.Length * 2];
 | 
			
		||||
                Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
 | 
			
		||||
                for (var i = localTables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
 | 
			
		||||
                for (var i = localTables.Locks.Length; i < newLocks.Length; i++)
 | 
			
		||||
                    newLocks[i] = new();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var newBuckets = new Node[newLength];
 | 
			
		||||
@@ -681,7 +715,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            budget = Math.Max(1, newBuckets.Length / newLocks.Length);
 | 
			
		||||
 | 
			
		||||
            // Replace tables with the new versions
 | 
			
		||||
            this.tables = new(newBuckets, newLocks, newCountPerLock);
 | 
			
		||||
            tables = new(newBuckets, newLocks, newCountPerLock);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
@@ -695,8 +729,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
        var elems = this.Where(predicate);
 | 
			
		||||
        var removed = 0;
 | 
			
		||||
        foreach (var elem in elems)
 | 
			
		||||
        {
 | 
			
		||||
            if (TryRemove(elem))
 | 
			
		||||
                removed++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return removed;
 | 
			
		||||
    }
 | 
			
		||||
@@ -726,7 +762,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                if (lockTaken) locksAcquired++;
 | 
			
		||||
                if (lockTaken)
 | 
			
		||||
                    locksAcquired++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -735,7 +772,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
 | 
			
		||||
    {
 | 
			
		||||
        Debug.Assert(fromInclusive <= toExclusive);
 | 
			
		||||
 | 
			
		||||
        for (var i = fromInclusive; i < toExclusive; i++) Monitor.Exit(tables.Locks[i]);
 | 
			
		||||
        for (var i = fromInclusive; i < toExclusive; i++)
 | 
			
		||||
            Monitor.Exit(tables.Locks[i]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void CopyToItems(T[] array, int index)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ using System.Collections;
 | 
			
		||||
namespace NadekoBot.Common.Collections;
 | 
			
		||||
 | 
			
		||||
public class IndexedCollection<T> : IList<T>
 | 
			
		||||
    where T : class, IIndexed 
 | 
			
		||||
    where T : class, IIndexed
 | 
			
		||||
{
 | 
			
		||||
    public List<T> Source { get; }
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +54,7 @@ public class IndexedCollection<T> : IList<T>
 | 
			
		||||
    public void Add(T item)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(item);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            item.Index = Source.Count;
 | 
			
		||||
@@ -93,8 +93,10 @@ public class IndexedCollection<T> : IList<T>
 | 
			
		||||
            if (Source.Remove(item))
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 0; i < Source.Count; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (Source[i].Index != i)
 | 
			
		||||
                        Source[i].Index = i;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
@@ -108,7 +110,8 @@ public class IndexedCollection<T> : IList<T>
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            Source.Insert(index, item);
 | 
			
		||||
            for (var i = index; i < Source.Count; i++) Source[i].Index = i;
 | 
			
		||||
            for (var i = index; i < Source.Count; i++)
 | 
			
		||||
                Source[i].Index = i;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +120,8 @@ public class IndexedCollection<T> : IList<T>
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            Source.RemoveAt(index);
 | 
			
		||||
            for (var i = index; i < Source.Count; i++) Source[i].Index = i;
 | 
			
		||||
            for (var i = index; i < Source.Count; i++)
 | 
			
		||||
                Source[i].Index = i;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -126,8 +130,10 @@ public class IndexedCollection<T> : IList<T>
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            for (var i = 0; i < Source.Count; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (Source[i].Index != i)
 | 
			
		||||
                    Source[i].Index = i;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ Used for cryptocurrency related commands.")]
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
 | 
			
		||||
    public string OsuApiKey { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Optional Trovo client id.
 | 
			
		||||
You should only use this if Trovo notifications stopped working or you're getting ratelimit errors.")]
 | 
			
		||||
    public string TrovoClientId { get; set; }
 | 
			
		||||
@@ -96,7 +96,11 @@ Windows default
 | 
			
		||||
        BotListToken = string.Empty;
 | 
			
		||||
        CleverbotApiKey = string.Empty;
 | 
			
		||||
        RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
 | 
			
		||||
        Db = new() { Type = "sqlite", ConnectionString = "Data Source=data/NadekoBot.db" };
 | 
			
		||||
        Db = new()
 | 
			
		||||
        {
 | 
			
		||||
            Type = "sqlite",
 | 
			
		||||
            ConnectionString = "Data Source=data/NadekoBot.db"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CoordinatorUrl = "http://localhost:3442";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,10 @@ public readonly struct kwum : IEquatable<kwum>
 | 
			
		||||
    {
 | 
			
		||||
        value = default;
 | 
			
		||||
        foreach (var c in input)
 | 
			
		||||
        {
 | 
			
		||||
            if (!IsValidChar(c))
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        value = new(input);
 | 
			
		||||
        return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace NadekoBot.Modules;
 | 
			
		||||
public abstract class NadekoModule : ModuleBase
 | 
			
		||||
{
 | 
			
		||||
    protected CultureInfo Culture { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Injected by Discord.net
 | 
			
		||||
    public IBotStrings Strings { get; set; }
 | 
			
		||||
    public CommandHandler _cmdHandler { get; set; }
 | 
			
		||||
@@ -88,7 +88,7 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _= Task.Run(() => msg.DeleteAsync());
 | 
			
		||||
            _ = Task.Run(() => msg.DeleteAsync());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -113,7 +113,7 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
 | 
			
		||||
        Task MessageReceived(SocketMessage arg)
 | 
			
		||||
        {
 | 
			
		||||
            _= Task.Run(() =>
 | 
			
		||||
            _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                if (arg is not SocketUserMessage userMsg
 | 
			
		||||
                    || userMsg.Channel is not ITextChannel
 | 
			
		||||
@@ -121,7 +121,8 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
                    || userMsg.Channel.Id != channelId)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                if (userInputTask.TrySetResult(arg.Content)) userMsg.DeleteAfter(1);
 | 
			
		||||
                if (userInputTask.TrySetResult(arg.Content))
 | 
			
		||||
                    userMsg.DeleteAfter(1);
 | 
			
		||||
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            });
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ public class EventPubSub : IPubSub
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
        where TData: notnull
 | 
			
		||||
        where TData : notnull
 | 
			
		||||
    {
 | 
			
		||||
        Func<object, ValueTask> localAction = obj => action((TData)obj);
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
@@ -30,7 +30,7 @@ public class EventPubSub : IPubSub
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
        where TData: notnull
 | 
			
		||||
        where TData : notnull
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
@@ -67,7 +67,8 @@ public class EventPubSub : IPubSub
 | 
			
		||||
                        // if our dictionary has no more elements after 
 | 
			
		||||
                        // removing the entry
 | 
			
		||||
                        // it's safe to remove it from the key's subscriptions
 | 
			
		||||
                        if (actions.Count == 0) _actions.Remove(key.Key);
 | 
			
		||||
                        if (actions.Count == 0)
 | 
			
		||||
                            _actions.Remove(key.Key);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ namespace NadekoBot.Common;
 | 
			
		||||
public interface IPubSub
 | 
			
		||||
{
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
        where TData: notnull;
 | 
			
		||||
        where TData : notnull;
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
        where TData: notnull;
 | 
			
		||||
        where TData : notnull;
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,11 @@ public class JsonSeria : ISeria
 | 
			
		||||
{
 | 
			
		||||
    private readonly JsonSerializerOptions _serializerOptions = new()
 | 
			
		||||
    {
 | 
			
		||||
        Converters = { new Rgba32Converter(), new CultureInfoConverter() }
 | 
			
		||||
        Converters =
 | 
			
		||||
        {
 | 
			
		||||
            new Rgba32Converter(),
 | 
			
		||||
            new CultureInfoConverter()
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public byte[] Serialize<T>(T data)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ public sealed class RedisPubSub : IPubSub
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var dataObj = _serializer.Deserialize<TData>(data);
 | 
			
		||||
                if(dataObj is not null)
 | 
			
		||||
                if (dataObj is not null)
 | 
			
		||||
                    await action(dataObj);
 | 
			
		||||
                else
 | 
			
		||||
                    Log.Warning("Publishing event {EventName} with a null value. This is not allowed",
 | 
			
		||||
 
 | 
			
		||||
@@ -63,10 +63,8 @@ public class ReplacementBuilder
 | 
			
		||||
            {
 | 
			
		||||
                var to = TimeZoneInfo.Local;
 | 
			
		||||
                if (g is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
 | 
			
		||||
                        to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
 | 
			
		||||
                       + to.StandardName.GetInitials();
 | 
			
		||||
 
 | 
			
		||||
@@ -20,10 +20,13 @@ public class Replacer
 | 
			
		||||
            return input;
 | 
			
		||||
 | 
			
		||||
        foreach (var (key, text) in _replacements)
 | 
			
		||||
        {
 | 
			
		||||
            if (input.Contains(key))
 | 
			
		||||
                input = input.Replace(key, text(), StringComparison.InvariantCulture);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var item in _regex) input = item.Regex.Replace(input, m => item.Replacement(m));
 | 
			
		||||
        foreach (var item in _regex)
 | 
			
		||||
            input = item.Regex.Replace(input, m => item.Replacement(m));
 | 
			
		||||
 | 
			
		||||
        return input;
 | 
			
		||||
    }
 | 
			
		||||
@@ -53,7 +56,8 @@ public class Replacer
 | 
			
		||||
        if (embedData.Author is not null)
 | 
			
		||||
            newEmbedData.Author = new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = Replace(embedData.Author.Name), IconUrl = Replace(embedData.Author.IconUrl)
 | 
			
		||||
                Name = Replace(embedData.Author.Name),
 | 
			
		||||
                IconUrl = Replace(embedData.Author.IconUrl)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        if (embedData.Fields is not null)
 | 
			
		||||
@@ -63,7 +67,9 @@ public class Replacer
 | 
			
		||||
            {
 | 
			
		||||
                var newF = new SmartTextEmbedField
 | 
			
		||||
                {
 | 
			
		||||
                    Name = Replace(f.Name), Value = Replace(f.Value), Inline = f.Inline
 | 
			
		||||
                    Name = Replace(f.Name),
 | 
			
		||||
                    Value = Replace(f.Value),
 | 
			
		||||
                    Inline = f.Inline
 | 
			
		||||
                };
 | 
			
		||||
                fields.Add(newF);
 | 
			
		||||
            }
 | 
			
		||||
@@ -74,7 +80,8 @@ public class Replacer
 | 
			
		||||
        if (embedData.Footer is not null)
 | 
			
		||||
            newEmbedData.Footer = new()
 | 
			
		||||
            {
 | 
			
		||||
                Text = Replace(embedData.Footer.Text), IconUrl = Replace(embedData.Footer.IconUrl)
 | 
			
		||||
                Text = Replace(embedData.Footer.Text),
 | 
			
		||||
                IconUrl = Replace(embedData.Footer.IconUrl)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        newEmbedData.Color = embedData.Color;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,16 +36,31 @@ public sealed record SmartEmbedText : SmartText
 | 
			
		||||
            Url = eb.Url,
 | 
			
		||||
            Thumbnail = eb.Thumbnail?.Url,
 | 
			
		||||
            Image = eb.Image?.Url,
 | 
			
		||||
            Author = eb.Author is { } ea ? new() { Name = ea.Name, Url = ea.Url, IconUrl = ea.IconUrl } : null,
 | 
			
		||||
            Footer = eb.Footer is { } ef ? new() { Text = ef.Text, IconUrl = ef.IconUrl } : null
 | 
			
		||||
            Author = eb.Author is { } ea
 | 
			
		||||
                ? new()
 | 
			
		||||
                {
 | 
			
		||||
                    Name = ea.Name,
 | 
			
		||||
                    Url = ea.Url,
 | 
			
		||||
                    IconUrl = ea.IconUrl
 | 
			
		||||
                }
 | 
			
		||||
                : null,
 | 
			
		||||
            Footer = eb.Footer is { } ef
 | 
			
		||||
                ? new()
 | 
			
		||||
                {
 | 
			
		||||
                    Text = ef.Text,
 | 
			
		||||
                    IconUrl = ef.IconUrl
 | 
			
		||||
                }
 | 
			
		||||
                : null
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (eb.Fields.Length > 0)
 | 
			
		||||
            set.Fields = eb.Fields.Select(field
 | 
			
		||||
                               => new SmartTextEmbedField
 | 
			
		||||
                                   {
 | 
			
		||||
                                       Inline = field.Inline, Name = field.Name, Value = field.Value
 | 
			
		||||
                                   })
 | 
			
		||||
                               {
 | 
			
		||||
                                   Inline = field.Inline,
 | 
			
		||||
                                   Name = field.Name,
 | 
			
		||||
                                   Value = field.Value
 | 
			
		||||
                               })
 | 
			
		||||
                           .ToArray();
 | 
			
		||||
 | 
			
		||||
        set.Color = eb.Color?.RawValue ?? 0;
 | 
			
		||||
@@ -91,8 +106,10 @@ public sealed record SmartEmbedText : SmartText
 | 
			
		||||
 | 
			
		||||
        if (Fields is not null)
 | 
			
		||||
            foreach (var f in Fields)
 | 
			
		||||
            {
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value))
 | 
			
		||||
                    embed.AddField(f.Name, f.Value, f.Inline);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return embed;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,10 @@ public abstract record SmartText
 | 
			
		||||
    public static SmartText operator +(SmartText text, string input)
 | 
			
		||||
        => text switch
 | 
			
		||||
        {
 | 
			
		||||
            SmartEmbedText set => set with { PlainText = set.PlainText + input },
 | 
			
		||||
            SmartEmbedText set => set with
 | 
			
		||||
            {
 | 
			
		||||
                PlainText = set.PlainText + input
 | 
			
		||||
            },
 | 
			
		||||
            SmartPlainText spt => new SmartPlainText(spt.Text + input),
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
			
		||||
        };
 | 
			
		||||
@@ -22,7 +25,10 @@ public abstract record SmartText
 | 
			
		||||
    public static SmartText operator +(string input, SmartText text)
 | 
			
		||||
        => text switch
 | 
			
		||||
        {
 | 
			
		||||
            SmartEmbedText set => set with { PlainText = input + set.PlainText },
 | 
			
		||||
            SmartEmbedText set => set with
 | 
			
		||||
            {
 | 
			
		||||
                PlainText = input + set.PlainText
 | 
			
		||||
            },
 | 
			
		||||
            SmartPlainText spt => new SmartPlainText(input + spt.Text),
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
			
		||||
        };
 | 
			
		||||
@@ -41,7 +47,7 @@ public abstract record SmartText
 | 
			
		||||
 | 
			
		||||
            smartEmbedText.NormalizeFields();
 | 
			
		||||
 | 
			
		||||
            if (!smartEmbedText.IsValid) 
 | 
			
		||||
            if (!smartEmbedText.IsValid)
 | 
			
		||||
                return new SmartPlainText(input);
 | 
			
		||||
 | 
			
		||||
            return smartEmbedText;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,8 @@ public class PermissionAction
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
    {
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType()) return false;
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return Value == ((PermissionAction)obj).Value;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,20 +18,23 @@ public class StoopidTime
 | 
			
		||||
    {
 | 
			
		||||
        var m = _regex.Match(input);
 | 
			
		||||
 | 
			
		||||
        if (m.Length == 0) throw new ArgumentException("Invalid string input format.");
 | 
			
		||||
        if (m.Length == 0)
 | 
			
		||||
            throw new ArgumentException("Invalid string input format.");
 | 
			
		||||
 | 
			
		||||
        var namesAndValues = new Dictionary<string, int>();
 | 
			
		||||
 | 
			
		||||
        foreach (var groupName in _regex.GetGroupNames())
 | 
			
		||||
        {
 | 
			
		||||
            if (groupName == "0") continue;
 | 
			
		||||
            if (groupName == "0")
 | 
			
		||||
                continue;
 | 
			
		||||
            if (!int.TryParse(m.Groups[groupName].Value, out var value))
 | 
			
		||||
            {
 | 
			
		||||
                namesAndValues[groupName] = 0;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (value < 1) throw new ArgumentException($"Invalid {groupName} value.");
 | 
			
		||||
            if (value < 1)
 | 
			
		||||
                throw new ArgumentException($"Invalid {groupName} value.");
 | 
			
		||||
 | 
			
		||||
            namesAndValues[groupName] = value;
 | 
			
		||||
        }
 | 
			
		||||
@@ -40,8 +43,13 @@ public class StoopidTime
 | 
			
		||||
            namesAndValues["hours"],
 | 
			
		||||
            namesAndValues["minutes"],
 | 
			
		||||
            namesAndValues["seconds"]);
 | 
			
		||||
        if (ts > TimeSpan.FromDays(90)) throw new ArgumentException("Time is too long.");
 | 
			
		||||
        if (ts > TimeSpan.FromDays(90))
 | 
			
		||||
            throw new ArgumentException("Time is too long.");
 | 
			
		||||
 | 
			
		||||
        return new() { Input = input, Time = ts };
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Input = input,
 | 
			
		||||
            Time = ts
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -37,7 +37,10 @@ public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
 | 
			
		||||
        if (module is null && input != "ACTUALEXPRESSIONS")
 | 
			
		||||
            return new(TypeReaderResult.FromError<ModuleOrCrInfo>(CommandError.ParseFailed, "No such module found."));
 | 
			
		||||
 | 
			
		||||
        return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo { Name = input }));
 | 
			
		||||
        return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
 | 
			
		||||
        {
 | 
			
		||||
            Name = input
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,8 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception)
 | 
			
		||||
        {
 | 
			
		||||
            return ValueTask.FromResult(TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, $"Invalid input: {input}"));
 | 
			
		||||
            return ValueTask.FromResult(
 | 
			
		||||
                TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, $"Invalid input: {input}"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,10 @@ public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
 | 
			
		||||
            {
 | 
			
		||||
                var isMultiLine = value.IndexOfAny(new[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
 | 
			
		||||
                if (isMultiLine)
 | 
			
		||||
                    eventInfo = new(eventInfo.Source) { Style = ScalarStyle.Literal };
 | 
			
		||||
                    eventInfo = new(eventInfo.Source)
 | 
			
		||||
                    {
 | 
			
		||||
                        Style = ScalarStyle.Literal
 | 
			
		||||
                    };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,14 +18,16 @@ public class YamlHelper
 | 
			
		||||
 | 
			
		||||
        foreach (var c in point)
 | 
			
		||||
        {
 | 
			
		||||
            if (!IsHex(c)) return point;
 | 
			
		||||
            if (!IsHex(c))
 | 
			
		||||
                return point;
 | 
			
		||||
 | 
			
		||||
            character = (character << 4) + AsHex(c);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check the value and write the character.
 | 
			
		||||
 | 
			
		||||
        if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF) return point;
 | 
			
		||||
        if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
 | 
			
		||||
            return point;
 | 
			
		||||
 | 
			
		||||
        return char.ConvertFromUtf32(character);
 | 
			
		||||
    }
 | 
			
		||||
@@ -35,9 +37,11 @@ public class YamlHelper
 | 
			
		||||
 | 
			
		||||
    public static int AsHex(char c)
 | 
			
		||||
    {
 | 
			
		||||
        if (c <= '9') return c - '0';
 | 
			
		||||
        if (c <= '9')
 | 
			
		||||
            return c - '0';
 | 
			
		||||
 | 
			
		||||
        if (c <= 'F') return c - 'A' + 10;
 | 
			
		||||
        if (c <= 'F')
 | 
			
		||||
            return c - 'A' + 10;
 | 
			
		||||
 | 
			
		||||
        return c - 'a' + 10;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,16 @@ public static class DiscordUserExtensions
 | 
			
		||||
                      TotalXp = 0,
 | 
			
		||||
                      CurrencyAmount = 0
 | 
			
		||||
                  },
 | 
			
		||||
                  old => new() { Username = username, Discriminator = discrim, AvatarId = avatarId },
 | 
			
		||||
                  () => new() { UserId = userId });
 | 
			
		||||
                  old => new()
 | 
			
		||||
                  {
 | 
			
		||||
                      Username = username,
 | 
			
		||||
                      Discriminator = discrim,
 | 
			
		||||
                      AvatarId = avatarId
 | 
			
		||||
                  },
 | 
			
		||||
                  () => new()
 | 
			
		||||
                  {
 | 
			
		||||
                      UserId = userId
 | 
			
		||||
                  });
 | 
			
		||||
 | 
			
		||||
    //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown
 | 
			
		||||
    public static DiscordUser GetOrCreateUser(
 | 
			
		||||
@@ -72,7 +80,8 @@ public static class DiscordUserExtensions
 | 
			
		||||
    public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
 | 
			
		||||
    {
 | 
			
		||||
        var items = users.AsQueryable().Where(x => ids.Contains(x.UserId));
 | 
			
		||||
        foreach (var item in items) item.CurrencyAmount = 0;
 | 
			
		||||
        foreach (var item in items)
 | 
			
		||||
            item.CurrencyAmount = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool TryUpdateCurrencyState(
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,16 @@ public static class GuildConfigExtensions
 | 
			
		||||
    private static List<WarningPunishment> DefaultWarnPunishments
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            new() { Count = 3, Punishment = PunishmentAction.Kick },
 | 
			
		||||
            new() { Count = 5, Punishment = PunishmentAction.Ban }
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Count = 3,
 | 
			
		||||
                Punishment = PunishmentAction.Kick
 | 
			
		||||
            },
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Count = 5,
 | 
			
		||||
                Punishment = PunishmentAction.Ban
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -105,7 +113,10 @@ public static class GuildConfigExtensions
 | 
			
		||||
 | 
			
		||||
        if (logSetting is null)
 | 
			
		||||
        {
 | 
			
		||||
            ctx.LogSettings.Add(logSetting = new() { GuildId = guildId });
 | 
			
		||||
            ctx.LogSettings.Add(logSetting = new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId
 | 
			
		||||
            });
 | 
			
		||||
            ctx.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +139,11 @@ public static class GuildConfigExtensions
 | 
			
		||||
 | 
			
		||||
        if (config is null) // if there is no guildconfig, create new one
 | 
			
		||||
        {
 | 
			
		||||
            ctx.GuildConfigs.Add(config = new() { GuildId = guildId, Permissions = Permissionv2.GetDefaultPermlist });
 | 
			
		||||
            ctx.GuildConfigs.Add(config = new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Permissions = Permissionv2.GetDefaultPermlist
 | 
			
		||||
            });
 | 
			
		||||
            ctx.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
        else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones
 | 
			
		||||
@@ -181,7 +196,11 @@ public static class GuildConfigExtensions
 | 
			
		||||
                  .Include(x => x.GenerateCurrencyChannelIds)
 | 
			
		||||
                  .Where(x => x.GenerateCurrencyChannelIds.Any())
 | 
			
		||||
                  .SelectMany(x => x.GenerateCurrencyChannelIds)
 | 
			
		||||
                  .Select(x => new GeneratingChannel { ChannelId = x.ChannelId, GuildId = x.GuildConfig.GuildId })
 | 
			
		||||
                  .Select(x => new GeneratingChannel
 | 
			
		||||
                  {
 | 
			
		||||
                      ChannelId = x.ChannelId,
 | 
			
		||||
                      GuildId = x.GuildConfig.GuildId
 | 
			
		||||
                  })
 | 
			
		||||
                  .ToArray();
 | 
			
		||||
 | 
			
		||||
    public class GeneratingChannel
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,11 @@ public static class MusicPlayerSettingsExtensions
 | 
			
		||||
 | 
			
		||||
        if (toReturn is null)
 | 
			
		||||
        {
 | 
			
		||||
            var newSettings = new MusicPlayerSettings { GuildId = guildId, PlayerRepeat = PlayerRepeatType.Queue };
 | 
			
		||||
            var newSettings = new MusicPlayerSettings
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                PlayerRepeat = PlayerRepeatType.Queue
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await settings.AddAsync(newSettings);
 | 
			
		||||
            return newSettings;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,9 @@ public static class NadekoExpressionExtensions
 | 
			
		||||
    public static IEnumerable<NadekoExpression> ForId(this DbSet<NadekoExpression> exprs, ulong id)
 | 
			
		||||
        => exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
 | 
			
		||||
 | 
			
		||||
    public static NadekoExpression GetByGuildIdAndInput(this DbSet<NadekoExpression> exprs, ulong? guildId, string input)
 | 
			
		||||
    public static NadekoExpression GetByGuildIdAndInput(
 | 
			
		||||
        this DbSet<NadekoExpression> exprs,
 | 
			
		||||
        ulong? guildId,
 | 
			
		||||
        string input)
 | 
			
		||||
        => exprs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,10 @@ public static class UserXpExtensions
 | 
			
		||||
        if (usr is null)
 | 
			
		||||
            ctx.Add(usr = new()
 | 
			
		||||
            {
 | 
			
		||||
                Xp = 0, UserId = userId, NotifyOnLevelUp = XpNotificationLocation.None, GuildId = guildId
 | 
			
		||||
                Xp = 0,
 | 
			
		||||
                UserId = userId,
 | 
			
		||||
                NotifyOnLevelUp = XpNotificationLocation.None,
 | 
			
		||||
                GuildId = guildId
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        return usr;
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,10 @@ public class Permissionv2 : DbEntity, IIndexed
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public static List<Permissionv2> GetDefaultPermlist
 | 
			
		||||
        => new() { AllowAllPerm };
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            AllowAllPerm
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum PrimaryPermissionType
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,8 @@ public class ShopEntryItem : DbEntity
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
    {
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType()) return false;
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType())
 | 
			
		||||
            return false;
 | 
			
		||||
        return ((ShopEntryItem)obj).Text == Text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@ public class SlowmodeIgnoredRole : DbEntity
 | 
			
		||||
    // override object.Equals
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
    {
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType()) return false;
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return ((SlowmodeIgnoredRole)obj).RoleId == RoleId;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@ public class SlowmodeIgnoredUser : DbEntity
 | 
			
		||||
    // override object.Equals
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
    {
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType()) return false;
 | 
			
		||||
        if (obj is null || GetType() != obj.GetType())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return ((SlowmodeIgnoredUser)obj).UserId == UserId;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@ public class WaifuInfo : DbEntity
 | 
			
		||||
        var waifuUsername = Waifu.Username.TrimTo(20);
 | 
			
		||||
        var claimerUsername = Claimer?.Username.TrimTo(20);
 | 
			
		||||
 | 
			
		||||
        if (ClaimerId is not null) claimer = $"{claimerUsername}#{Claimer.Discriminator}";
 | 
			
		||||
        if (ClaimerId is not null)
 | 
			
		||||
            claimer = $"{claimerUsername}#{Claimer.Discriminator}";
 | 
			
		||||
        if (AffinityId is null)
 | 
			
		||||
            status = $"... but {waifuUsername}'s heart is empty";
 | 
			
		||||
        else if (AffinityId == ClaimerId)
 | 
			
		||||
@@ -58,7 +59,8 @@ public class WaifuLbResult
 | 
			
		||||
        var waifuUsername = Username.TrimTo(20);
 | 
			
		||||
        var claimerUsername = Claimer?.TrimTo(20);
 | 
			
		||||
 | 
			
		||||
        if (Claimer is not null) claimer = $"{claimerUsername}#{ClaimerDiscrim}";
 | 
			
		||||
        if (Claimer is not null)
 | 
			
		||||
            claimer = $"{claimerUsername}#{ClaimerDiscrim}";
 | 
			
		||||
        if (Affinity is null)
 | 
			
		||||
            status = $"... but {waifuUsername}'s heart is empty";
 | 
			
		||||
        else if (Affinity + AffinityDiscrim == Claimer + ClaimerDiscrim)
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ public class NadekoContext : DbContext
 | 
			
		||||
    public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
 | 
			
		||||
    public DbSet<AutoTranslateChannel> AutoTranslateChannels { get; set; }
 | 
			
		||||
    public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public DbSet<Permissionv2> Permissions { get; set; }
 | 
			
		||||
 | 
			
		||||
    public NadekoContext(DbContextOptions<NadekoContext> options)
 | 
			
		||||
@@ -95,7 +95,12 @@ public class NadekoContext : DbContext
 | 
			
		||||
                    .HasForeignKey<AntiAltSetting>(x => x.GuildConfigId)
 | 
			
		||||
                    .OnDelete(DeleteBehavior.Cascade);
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<FeedSub>().HasAlternateKey(x => new { x.GuildConfigId, x.Url });
 | 
			
		||||
        modelBuilder.Entity<FeedSub>()
 | 
			
		||||
                    .HasAlternateKey(x => new
 | 
			
		||||
                    {
 | 
			
		||||
                        x.GuildConfigId,
 | 
			
		||||
                        x.Url
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.MessageId).IsUnique();
 | 
			
		||||
 | 
			
		||||
@@ -115,7 +120,12 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
 | 
			
		||||
 | 
			
		||||
        selfassignableRolesEntity.HasIndex(s => new { s.GuildId, s.RoleId }).IsUnique();
 | 
			
		||||
        selfassignableRolesEntity.HasIndex(s => new
 | 
			
		||||
                                 {
 | 
			
		||||
                                     s.GuildId,
 | 
			
		||||
                                     s.RoleId
 | 
			
		||||
                                 })
 | 
			
		||||
                                 .IsUnique();
 | 
			
		||||
 | 
			
		||||
        selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +170,7 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
            du.Property(x => x.TotalXp)
 | 
			
		||||
              .HasDefaultValue(0);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            du.Property(x => x.CurrencyAmount)
 | 
			
		||||
              .HasDefaultValue(0);
 | 
			
		||||
 | 
			
		||||
@@ -196,7 +206,12 @@ public class NadekoContext : DbContext
 | 
			
		||||
        #region XpStats
 | 
			
		||||
 | 
			
		||||
        var xps = modelBuilder.Entity<UserXpStats>();
 | 
			
		||||
        xps.HasIndex(x => new { x.UserId, x.GuildId }).IsUnique();
 | 
			
		||||
        xps.HasIndex(x => new
 | 
			
		||||
           {
 | 
			
		||||
               x.UserId,
 | 
			
		||||
               x.GuildId
 | 
			
		||||
           })
 | 
			
		||||
           .IsUnique();
 | 
			
		||||
 | 
			
		||||
        xps.Property(x => x.LastLevelUp)
 | 
			
		||||
           .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local));
 | 
			
		||||
@@ -216,7 +231,13 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        #region XpRoleReward
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<XpRoleReward>().HasIndex(x => new { x.XpSettingsId, x.Level }).IsUnique();
 | 
			
		||||
        modelBuilder.Entity<XpRoleReward>()
 | 
			
		||||
                    .HasIndex(x => new
 | 
			
		||||
                    {
 | 
			
		||||
                        x.XpSettingsId,
 | 
			
		||||
                        x.Level
 | 
			
		||||
                    })
 | 
			
		||||
                    .IsUnique();
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
@@ -226,19 +247,33 @@ public class NadekoContext : DbContext
 | 
			
		||||
        ci.HasOne(x => x.Owner).WithOne().HasForeignKey<ClubInfo>(x => x.OwnerId);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        ci.HasAlternateKey(x => new { x.Name, x.Discrim });
 | 
			
		||||
        ci.HasAlternateKey(x => new
 | 
			
		||||
        {
 | 
			
		||||
            x.Name,
 | 
			
		||||
            x.Discrim
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        #region ClubManytoMany
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<ClubApplicants>().HasKey(t => new { t.ClubId, t.UserId });
 | 
			
		||||
        modelBuilder.Entity<ClubApplicants>()
 | 
			
		||||
                    .HasKey(t => new
 | 
			
		||||
                    {
 | 
			
		||||
                        t.ClubId,
 | 
			
		||||
                        t.UserId
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<ClubApplicants>().HasOne(pt => pt.User).WithMany();
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<ClubApplicants>().HasOne(pt => pt.Club).WithMany(x => x.Applicants);
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<ClubBans>().HasKey(t => new { t.ClubId, t.UserId });
 | 
			
		||||
        modelBuilder.Entity<ClubBans>()
 | 
			
		||||
                    .HasKey(t => new
 | 
			
		||||
                    {
 | 
			
		||||
                        t.ClubId,
 | 
			
		||||
                        t.UserId
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<ClubBans>().HasOne(pt => pt.User).WithMany();
 | 
			
		||||
 | 
			
		||||
@@ -264,12 +299,10 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
            e.Property(x => x.Type)
 | 
			
		||||
             .IsRequired();
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            e.Property(x => x.Extra)
 | 
			
		||||
             .IsRequired();
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
@@ -281,7 +314,13 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        #region GroupName
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<GroupName>().HasIndex(x => new { x.GuildConfigId, x.Number }).IsUnique();
 | 
			
		||||
        modelBuilder.Entity<GroupName>()
 | 
			
		||||
                    .HasIndex(x => new
 | 
			
		||||
                    {
 | 
			
		||||
                        x.GuildConfigId,
 | 
			
		||||
                        x.Number
 | 
			
		||||
                    })
 | 
			
		||||
                    .IsUnique();
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<GroupName>()
 | 
			
		||||
                    .HasOne(x => x.GuildConfig)
 | 
			
		||||
@@ -298,7 +337,13 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        #region Perm Override
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<DiscordPermOverride>().HasIndex(x => new { x.GuildId, x.Command }).IsUnique();
 | 
			
		||||
        modelBuilder.Entity<DiscordPermOverride>()
 | 
			
		||||
                    .HasIndex(x => new
 | 
			
		||||
                    {
 | 
			
		||||
                        x.GuildId,
 | 
			
		||||
                        x.Command
 | 
			
		||||
                    })
 | 
			
		||||
                    .IsUnique();
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
@@ -329,7 +374,12 @@ public class NadekoContext : DbContext
 | 
			
		||||
                                              .OnDelete(DeleteBehavior.Cascade));
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<IgnoredLogItem>(ili => ili
 | 
			
		||||
                                                   .HasIndex(x => new { x.LogSettingId, x.LogItemId, x.ItemType })
 | 
			
		||||
                                                   .HasIndex(x => new
 | 
			
		||||
                                                   {
 | 
			
		||||
                                                       x.LogSettingId,
 | 
			
		||||
                                                       x.LogItemId,
 | 
			
		||||
                                                       x.ItemType
 | 
			
		||||
                                                   })
 | 
			
		||||
                                                   .IsUnique());
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
@@ -345,8 +395,11 @@ public class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        atch.HasMany(x => x.Users).WithOne(x => x.Channel).OnDelete(DeleteBehavior.Cascade);
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<AutoTranslateUser>(atu => atu.HasAlternateKey(x => new { x.ChannelId, x.UserId }));
 | 
			
		||||
        
 | 
			
		||||
        modelBuilder.Entity<AutoTranslateUser>(atu => atu.HasAlternateKey(x => new
 | 
			
		||||
        {
 | 
			
		||||
            x.ChannelId,
 | 
			
		||||
            x.UserId
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if DEBUG
 | 
			
		||||
 
 | 
			
		||||
@@ -314,7 +314,9 @@ public partial class Administration : NadekoModule<AdministrationService>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (time is null)
 | 
			
		||||
        {
 | 
			
		||||
            await msg.DeleteAsync();
 | 
			
		||||
        }
 | 
			
		||||
        else if (time.Time <= TimeSpan.FromDays(7))
 | 
			
		||||
        {
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public class AdministrationService : INService
 | 
			
		||||
 | 
			
		||||
    private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            if (msg.Channel is not SocketTextChannel channel)
 | 
			
		||||
                return;
 | 
			
		||||
@@ -97,7 +97,10 @@ public class AdministrationService : INService
 | 
			
		||||
            {
 | 
			
		||||
                if (old is null)
 | 
			
		||||
                {
 | 
			
		||||
                    old = new() { ChannelId = chId };
 | 
			
		||||
                    old = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        ChannelId = chId
 | 
			
		||||
                    };
 | 
			
		||||
                    conf.DelMsgOnCmdChannels.Add(old);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -126,6 +129,7 @@ public class AdministrationService : INService
 | 
			
		||||
        if (!users.Any())
 | 
			
		||||
            return;
 | 
			
		||||
        foreach (var u in users)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await u.ModifyAsync(usr => usr.Deaf = value);
 | 
			
		||||
@@ -134,6 +138,7 @@ public class AdministrationService : INService
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task EditMessage(
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,9 @@ public sealed class AutoAssignRoleService : INService
 | 
			
		||||
    private readonly Channel<SocketGuildUser> _assignQueue = Channel.CreateBounded<SocketGuildUser>(
 | 
			
		||||
        new BoundedChannelOptions(100)
 | 
			
		||||
        {
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest, SingleReader = true, SingleWriter = false
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest,
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
@@ -118,7 +120,10 @@ public sealed class AutoAssignRoleService : INService
 | 
			
		||||
 | 
			
		||||
        await uow.GuildConfigs.AsNoTracking()
 | 
			
		||||
                 .Where(x => x.GuildId == guildId)
 | 
			
		||||
                 .UpdateAsync(_ => new() { AutoAssignRoleIds = null });
 | 
			
		||||
                 .UpdateAsync(_ => new()
 | 
			
		||||
                 {
 | 
			
		||||
                     AutoAssignRoleIds = null
 | 
			
		||||
                 });
 | 
			
		||||
 | 
			
		||||
        _autoAssignableRoles.TryRemove(guildId, out _);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,8 @@ namespace NadekoBot.Modules.Administration
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
 | 
			
		||||
 | 
			
		||||
                if (!await PromptUserConfirmAsync(embed)) return;
 | 
			
		||||
                if (!await PromptUserConfirmAsync(embed))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await _service.PurgeUserAsync(userId);
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,11 @@ DELETE FROM Clubs;";
 | 
			
		||||
 | 
			
		||||
    public SelectResult SelectSql(string sql)
 | 
			
		||||
    {
 | 
			
		||||
        var result = new SelectResult { ColumnNames = new(), Results = new() };
 | 
			
		||||
        var result = new SelectResult
 | 
			
		||||
        {
 | 
			
		||||
            ColumnNames = new(),
 | 
			
		||||
            Results = new()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conn = uow.Database.GetDbConnection();
 | 
			
		||||
@@ -61,7 +65,8 @@ DELETE FROM Clubs;";
 | 
			
		||||
        using var reader = cmd.ExecuteReader();
 | 
			
		||||
        if (reader.HasRows)
 | 
			
		||||
        {
 | 
			
		||||
            for (var i = 0; i < reader.FieldCount; i++) result.ColumnNames.Add(reader.GetName(i));
 | 
			
		||||
            for (var i = 0; i < reader.FieldCount; i++)
 | 
			
		||||
                result.ColumnNames.Add(reader.GetName(i));
 | 
			
		||||
            while (reader.Read())
 | 
			
		||||
            {
 | 
			
		||||
                var obj = new object[reader.FieldCount];
 | 
			
		||||
@@ -93,13 +98,19 @@ DELETE FROM Clubs;";
 | 
			
		||||
            await uow.Set<WaifuInfo>()
 | 
			
		||||
                     .AsQueryable()
 | 
			
		||||
                     .Where(x => x.Claimer.UserId == userId)
 | 
			
		||||
                     .UpdateAsync(x => new() { ClaimerId = null });
 | 
			
		||||
                     .UpdateAsync(x => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         ClaimerId = null
 | 
			
		||||
                     });
 | 
			
		||||
 | 
			
		||||
            // all affinities set to this waifu are reset
 | 
			
		||||
            await uow.Set<WaifuInfo>()
 | 
			
		||||
                     .AsQueryable()
 | 
			
		||||
                     .Where(x => x.Affinity.UserId == userId)
 | 
			
		||||
                     .UpdateAsync(x => new() { AffinityId = null });
 | 
			
		||||
                     .UpdateAsync(x => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         AffinityId = null
 | 
			
		||||
                     });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete guild xp
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ public class GameVoiceChannelService : INService
 | 
			
		||||
 | 
			
		||||
    private Task OnPresenceUpdate(SocketUser socketUser, SocketPresence before, SocketPresence after)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -34,20 +34,18 @@ public class GameVoiceChannelService : INService
 | 
			
		||||
                    return;
 | 
			
		||||
                // if the user is in the voice channel and that voice channel is gvc
 | 
			
		||||
 | 
			
		||||
                if (newUser.VoiceChannel is not { } vc 
 | 
			
		||||
                if (newUser.VoiceChannel is not { } vc
 | 
			
		||||
                    || !GameVoiceChannels.Contains(vc.Id))
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
                     //if the activity has changed, and is a playi1ng activity
 | 
			
		||||
                     foreach (var activity in after.Activities)
 | 
			
		||||
                     {
 | 
			
		||||
                         if (activity is { Type: ActivityType.Playing })
 | 
			
		||||
                         {
 | 
			
		||||
                             //trigger gvc
 | 
			
		||||
                             if (await TriggerGvc(newUser, activity.Name))
 | 
			
		||||
                                 return;
 | 
			
		||||
                         }
 | 
			
		||||
                     }
 | 
			
		||||
 | 
			
		||||
                //if the activity has changed, and is a playi1ng activity
 | 
			
		||||
                foreach (var activity in after.Activities)
 | 
			
		||||
                {
 | 
			
		||||
                    if (activity is { Type: ActivityType.Playing })
 | 
			
		||||
                        //trigger gvc
 | 
			
		||||
                        if (await TriggerGvc(newUser, activity.Name))
 | 
			
		||||
                            return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
@@ -82,7 +80,7 @@ public class GameVoiceChannelService : INService
 | 
			
		||||
 | 
			
		||||
    private Task OnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -94,7 +92,7 @@ public class GameVoiceChannelService : INService
 | 
			
		||||
 | 
			
		||||
                if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id))
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                foreach (var game in gUser.Activities.Select(x => x.Name))
 | 
			
		||||
                {
 | 
			
		||||
                    if (await TriggerGvc(gUser, game))
 | 
			
		||||
 
 | 
			
		||||
@@ -190,7 +190,8 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            await _service.ByeTest((ITextChannel)ctx.Channel, user);
 | 
			
		||||
            var enabled = _service.GetByeEnabled(ctx.Guild.Id);
 | 
			
		||||
            if (!enabled) await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
 | 
			
		||||
            if (!enabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
@@ -203,7 +204,8 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            await _service.GreetTest((ITextChannel)ctx.Channel, user);
 | 
			
		||||
            var enabled = _service.GetGreetEnabled(ctx.Guild.Id);
 | 
			
		||||
            if (!enabled) await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
 | 
			
		||||
            if (!enabled)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
            await Task.Delay(2000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
 | 
			
		||||
    {
 | 
			
		||||
        // if user is a new booster
 | 
			
		||||
@@ -61,7 +61,8 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
                && newDate > oldDate))
 | 
			
		||||
        {
 | 
			
		||||
            var conf = GetOrAddSettingsForGuild(newUser.Guild.Id);
 | 
			
		||||
            if (!conf.SendBoostMessage) return Task.CompletedTask;
 | 
			
		||||
            if (!conf.SendBoostMessage)
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
            _ = Task.Run(TriggerBoostMessage(conf, newUser));
 | 
			
		||||
        }
 | 
			
		||||
@@ -85,7 +86,8 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var toDelete = await channel.SendAsync(rep.Replace(toSend));
 | 
			
		||||
                if (conf.BoostMessageDeleteAfter > 0) toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
 | 
			
		||||
                if (conf.BoostMessageDeleteAfter > 0)
 | 
			
		||||
                    toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
@@ -107,13 +109,14 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task OnUserLeft(SocketGuild guild, SocketUser user)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var conf = GetOrAddSettingsForGuild(guild.Id);
 | 
			
		||||
 | 
			
		||||
                if (!conf.SendChannelByeMessage) return;
 | 
			
		||||
                if (!conf.SendChannelByeMessage)
 | 
			
		||||
                    return;
 | 
			
		||||
                var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId);
 | 
			
		||||
 | 
			
		||||
                if (channel is null) //maybe warn the server owner that the channel is missing
 | 
			
		||||
@@ -187,7 +190,8 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await channel.SendAsync(text);
 | 
			
		||||
            if (conf.AutoDeleteByeMessagesTimer > 0) toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
 | 
			
		||||
            if (conf.AutoDeleteByeMessagesTimer > 0)
 | 
			
		||||
                toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -214,7 +218,8 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await channel.SendAsync(text);
 | 
			
		||||
            if (conf.AutoDeleteGreetMessagesTimer > 0) toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
 | 
			
		||||
            if (conf.AutoDeleteGreetMessagesTimer > 0)
 | 
			
		||||
                toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -229,14 +234,14 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
            // probably the best thing to do is to drop newest (raiding) users
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropNewest
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
        await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource));
 | 
			
		||||
        return await completionSource.Task;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
@@ -248,15 +253,17 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
            var text = SmartText.CreateFrom(conf.DmGreetMessageText);
 | 
			
		||||
            text = rep.Replace(text);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (text is SmartPlainText pt)
 | 
			
		||||
            {
 | 
			
		||||
                text = new SmartEmbedText() { PlainText = pt.Text };
 | 
			
		||||
            }
 | 
			
		||||
                text = new SmartEmbedText()
 | 
			
		||||
                {
 | 
			
		||||
                    PlainText = pt.Text
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            ((SmartEmbedText)text).Footer = new()
 | 
			
		||||
            {
 | 
			
		||||
                Text = $"This message was sent from {user.Guild} server.", IconUrl = user.Guild.IconUrl
 | 
			
		||||
                Text = $"This message was sent from {user.Guild} server.",
 | 
			
		||||
                IconUrl = user.Guild.IconUrl
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await user.SendAsync(text);
 | 
			
		||||
@@ -271,7 +278,7 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task OnUserJoined(IGuildUser user)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -308,9 +315,7 @@ public class GreetService : INService, IReadyExecutor
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (conf.SendDmGreetMessage)
 | 
			
		||||
                {
 | 
			
		||||
                    await GreetDmUser(conf, user);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,9 @@ public sealed class ImageOnlyChannelService : IEarlyBehavior
 | 
			
		||||
    private readonly Channel<IUserMessage> _deleteQueue = Channel.CreateBounded<IUserMessage>(
 | 
			
		||||
        new BoundedChannelOptions(100)
 | 
			
		||||
        {
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest, SingleReader = true, SingleWriter = false
 | 
			
		||||
            FullMode = BoundedChannelFullMode.DropOldest,
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +80,11 @@ public sealed class ImageOnlyChannelService : IEarlyBehavior
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            uow.ImageOnlyChannels.Add(new() { GuildId = guildId, ChannelId = channelId });
 | 
			
		||||
            uow.ImageOnlyChannels.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                ChannelId = channelId
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            channels = _enabledOn.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
 | 
			
		||||
            channels.Add(channelId);
 | 
			
		||||
 
 | 
			
		||||
@@ -127,13 +127,13 @@ public class MuteService : INService
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _= Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
                                                                   $"You've been muted in {user.Guild} server")
 | 
			
		||||
                                                               .AddField("Mute Type", type.ToString())
 | 
			
		||||
                                                               .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                               .AddField("Reason", reason)
 | 
			
		||||
                                                               .Build()));
 | 
			
		||||
        _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                           .WithDescription(
 | 
			
		||||
                                                               $"You've been muted in {user.Guild} server")
 | 
			
		||||
                                                           .AddField("Mute Type", type.ToString())
 | 
			
		||||
                                                           .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                           .AddField("Reason", reason)
 | 
			
		||||
                                                           .Build()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void OnUserUnmuted(
 | 
			
		||||
@@ -145,13 +145,13 @@ public class MuteService : INService
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(reason))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _= Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
                                                                   $"You've been unmuted in {user.Guild} server")
 | 
			
		||||
                                                               .AddField("Unmute Type", type.ToString())
 | 
			
		||||
                                                               .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                               .AddField("Reason", reason)
 | 
			
		||||
                                                               .Build()));
 | 
			
		||||
        _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
 | 
			
		||||
                                                           .WithDescription(
 | 
			
		||||
                                                               $"You've been unmuted in {user.Guild} server")
 | 
			
		||||
                                                           .AddField("Unmute Type", type.ToString())
 | 
			
		||||
                                                           .AddField("Moderator", mod.ToString())
 | 
			
		||||
                                                           .AddField("Reason", reason)
 | 
			
		||||
                                                           .Build()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Client_UserJoined(IGuildUser usr)
 | 
			
		||||
@@ -162,7 +162,7 @@ public class MuteService : INService
 | 
			
		||||
 | 
			
		||||
            if (muted is null || !muted.Contains(usr.Id))
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            _= Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute"));
 | 
			
		||||
            _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute"));
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -200,7 +200,10 @@ public class MuteService : INService
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(usr.Guild.Id,
 | 
			
		||||
                    set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
 | 
			
		||||
                config.MutedUsers.Add(new() { UserId = usr.Id });
 | 
			
		||||
                config.MutedUsers.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = usr.Id
 | 
			
		||||
                });
 | 
			
		||||
                if (MutedUsers.TryGetValue(usr.Guild.Id, out var muted))
 | 
			
		||||
                    muted.Add(usr.Id);
 | 
			
		||||
 | 
			
		||||
@@ -242,9 +245,13 @@ public class MuteService : INService
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(guildId,
 | 
			
		||||
                    set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
 | 
			
		||||
                var match = new MutedUserId { UserId = usrId };
 | 
			
		||||
                var match = new MutedUserId
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = usrId
 | 
			
		||||
                };
 | 
			
		||||
                var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match));
 | 
			
		||||
                if (toRemove is not null) uow.Remove(toRemove);
 | 
			
		||||
                if (toRemove is not null)
 | 
			
		||||
                    uow.Remove(toRemove);
 | 
			
		||||
                if (MutedUsers.TryGetValue(guildId, out var muted))
 | 
			
		||||
                    muted.TryRemove(usrId);
 | 
			
		||||
 | 
			
		||||
@@ -308,6 +315,7 @@ public class MuteService : INService
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        foreach (var toOverwrite in await guild.GetTextChannelsAsync())
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id
 | 
			
		||||
@@ -322,6 +330,7 @@ public class MuteService : INService
 | 
			
		||||
            {
 | 
			
		||||
                // ignored
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return muteRole;
 | 
			
		||||
    }
 | 
			
		||||
@@ -339,7 +348,8 @@ public class MuteService : INService
 | 
			
		||||
            var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers));
 | 
			
		||||
            config.UnmuteTimers.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id, UnmuteAt = DateTime.UtcNow + after
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UnmuteAt = DateTime.UtcNow + after
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
@@ -359,7 +369,8 @@ public class MuteService : INService
 | 
			
		||||
            var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer));
 | 
			
		||||
            config.UnbanTimer.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id, UnbanAt = DateTime.UtcNow + after
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UnbanAt = DateTime.UtcNow + after
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
@@ -379,7 +390,9 @@ public class MuteService : INService
 | 
			
		||||
            var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer));
 | 
			
		||||
            config.UnroleTimer.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = user.Id, UnbanAt = DateTime.UtcNow + after, RoleId = role.Id
 | 
			
		||||
                UserId = user.Id,
 | 
			
		||||
                UnbanAt = DateTime.UtcNow + after,
 | 
			
		||||
                RoleId = role.Id
 | 
			
		||||
            }); // add teh unmute timer to the database
 | 
			
		||||
            uow.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
@@ -401,26 +414,24 @@ public class MuteService : INService
 | 
			
		||||
        var toAdd = new Timer(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                if (type == TimerType.Ban)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type);
 | 
			
		||||
                        StopTimer(guildId, userId, type);
 | 
			
		||||
                        var guild = _client.GetGuild(guildId); // load the guild
 | 
			
		||||
                        if (guild is not null) await guild.RemoveBanAsync(userId);
 | 
			
		||||
                        if (guild is not null)
 | 
			
		||||
                            await guild.RemoveBanAsync(userId);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unban user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (type == TimerType.AddRole)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (roleId is null)
 | 
			
		||||
                            return;
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type);
 | 
			
		||||
                        StopTimer(guildId, userId, type);
 | 
			
		||||
                        var guild = _client.GetGuild(guildId);
 | 
			
		||||
@@ -433,9 +444,7 @@ public class MuteService : INService
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex, "Couldn't remove role from user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        // unmute the user, this will also remove the timer from the db
 | 
			
		||||
@@ -446,7 +455,6 @@ public class MuteService : INService
 | 
			
		||||
                        RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db
 | 
			
		||||
                        Log.Warning(ex, "Couldn't unmute user {UserId} in guild {GuildId}", userId, guildId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            null,
 | 
			
		||||
            after,
 | 
			
		||||
@@ -467,7 +475,8 @@ public class MuteService : INService
 | 
			
		||||
        if (!UnTimers.TryGetValue(guildId, out var userTimer))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (userTimer.TryRemove((userId, type), out var removed)) removed.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
        if (userTimer.TryRemove((userId, type), out var removed))
 | 
			
		||||
            removed.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type)
 | 
			
		||||
@@ -485,7 +494,8 @@ public class MuteService : INService
 | 
			
		||||
            toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (toDelete is not null) uow.Remove(toDelete);
 | 
			
		||||
        if (toDelete is not null)
 | 
			
		||||
            uow.Remove(toDelete);
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -41,7 +41,7 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            if (!result)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            await _service.ClearAllOverrides(ctx.Guild.Id);
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.perm_override_all);
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,13 @@ public class DiscordPermOverrideService : INService, ILateBlocker
 | 
			
		||||
                            .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
 | 
			
		||||
 | 
			
		||||
        if (over is null)
 | 
			
		||||
            uow.Set<DiscordPermOverride>().Add(over = new() { Command = commandName, Perm = perm, GuildId = guildId });
 | 
			
		||||
            uow.Set<DiscordPermOverride>()
 | 
			
		||||
               .Add(over = new()
 | 
			
		||||
               {
 | 
			
		||||
                   Command = commandName,
 | 
			
		||||
                   Perm = perm,
 | 
			
		||||
                   GuildId = guildId
 | 
			
		||||
               });
 | 
			
		||||
        else
 | 
			
		||||
            over.Perm = perm;
 | 
			
		||||
 | 
			
		||||
@@ -77,7 +83,8 @@ public class DiscordPermOverrideService : INService, ILateBlocker
 | 
			
		||||
        uow.RemoveRange(overrides);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        foreach (var over in overrides) _overrides.TryRemove((guildId, over.Command), out _);
 | 
			
		||||
        foreach (var over in overrides)
 | 
			
		||||
            _overrides.TryRemove((guildId, over.Command), out _);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RemoveOverride(ulong guildId, string commandName)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,7 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
 | 
			
		||||
        _selfService = selfService;
 | 
			
		||||
 | 
			
		||||
        if (client.ShardId == 0)
 | 
			
		||||
        {
 | 
			
		||||
            _rep = new ReplacementBuilder().WithClient(client).WithProviders(phProviders).Build();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
@@ -37,7 +35,7 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!_bss.Data.RotateStatuses) 
 | 
			
		||||
                if (!_bss.Data.RotateStatuses)
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
 | 
			
		||||
@@ -82,7 +80,11 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
 | 
			
		||||
    public async Task AddPlaying(ActivityType t, string status)
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var toAdd = new RotatingPlayingStatus { Status = status, Type = t };
 | 
			
		||||
        var toAdd = new RotatingPlayingStatus
 | 
			
		||||
        {
 | 
			
		||||
            Status = status,
 | 
			
		||||
            Type = t
 | 
			
		||||
        };
 | 
			
		||||
        uow.Add(toAdd);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,10 @@ public partial class Administration
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
        public async partial Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] StoopidTime punishTime = null)
 | 
			
		||||
        public async partial Task AntiAlt(
 | 
			
		||||
            StoopidTime minAge,
 | 
			
		||||
            PunishmentAction action,
 | 
			
		||||
            [Leftover] StoopidTime punishTime = null)
 | 
			
		||||
        {
 | 
			
		||||
            var minAgeMinutes = (int)minAge.Time.TotalMinutes;
 | 
			
		||||
            var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
 | 
			
		||||
@@ -120,7 +123,8 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, action, time);
 | 
			
		||||
 | 
			
		||||
            if (stats is null) return;
 | 
			
		||||
            if (stats is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")),
 | 
			
		||||
                $"{ctx.User.Mention} {GetAntiRaidString(stats)}");
 | 
			
		||||
@@ -244,7 +248,8 @@ public partial class Administration
 | 
			
		||||
                ignoredString = "none";
 | 
			
		||||
 | 
			
		||||
            var add = string.Empty;
 | 
			
		||||
            if (settings.MuteTime > 0) add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})";
 | 
			
		||||
            if (settings.MuteTime > 0)
 | 
			
		||||
                add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})";
 | 
			
		||||
 | 
			
		||||
            return GetText(strs.spam_stats(Format.Bold(settings.MessageThreshold.ToString()),
 | 
			
		||||
                Format.Bold(settings.Action + add),
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,11 @@ public class ProtectionService : INService
 | 
			
		||||
    private readonly UserPunishService _punishService;
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<PunishQueueItem> _punishUserQueue =
 | 
			
		||||
        Channel.CreateUnbounded<PunishQueueItem>(new() { SingleReader = true, SingleWriter = false });
 | 
			
		||||
        Channel.CreateUnbounded<PunishQueueItem>(new()
 | 
			
		||||
        {
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    public ProtectionService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
@@ -51,7 +55,8 @@ public class ProtectionService : INService
 | 
			
		||||
                             .Where(x => ids.Contains(x.GuildId))
 | 
			
		||||
                             .ToList();
 | 
			
		||||
 | 
			
		||||
            foreach (var gc in configs) Initialize(gc);
 | 
			
		||||
            foreach (var gc in configs)
 | 
			
		||||
                Initialize(gc);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _client.MessageReceived += HandleAntiSpam;
 | 
			
		||||
@@ -94,7 +99,7 @@ public class ProtectionService : INService
 | 
			
		||||
 | 
			
		||||
    private Task _client_LeftGuild(SocketGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            TryStopAntiRaid(guild.Id);
 | 
			
		||||
            TryStopAntiSpam(guild.Id);
 | 
			
		||||
@@ -123,12 +128,18 @@ public class ProtectionService : INService
 | 
			
		||||
 | 
			
		||||
        if (raid is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var raidStats = new AntiRaidStats { AntiRaidSettings = raid };
 | 
			
		||||
            var raidStats = new AntiRaidStats
 | 
			
		||||
            {
 | 
			
		||||
                AntiRaidSettings = raid
 | 
			
		||||
            };
 | 
			
		||||
            _antiRaidGuilds[gc.GuildId] = raidStats;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (spam is not null)
 | 
			
		||||
            _antiSpamGuilds[gc.GuildId] = new() { AntiSpamSettings = spam };
 | 
			
		||||
            _antiSpamGuilds[gc.GuildId] = new()
 | 
			
		||||
            {
 | 
			
		||||
                AntiSpamSettings = spam
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        var alt = gc.AntiAltSetting;
 | 
			
		||||
        if (alt is not null)
 | 
			
		||||
@@ -202,12 +213,16 @@ public class ProtectionService : INService
 | 
			
		||||
 | 
			
		||||
        if (msg.Channel is not ITextChannel channel)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
 | 
			
		||||
                    || spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new() { ChannelId = channel.Id }))
 | 
			
		||||
                    || spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        ChannelId = channel.Id
 | 
			
		||||
                    }))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
 | 
			
		||||
@@ -251,6 +266,7 @@ public class ProtectionService : INService
 | 
			
		||||
            gus[0].Guild.Name);
 | 
			
		||||
 | 
			
		||||
        foreach (var gu in gus)
 | 
			
		||||
        {
 | 
			
		||||
            await _punishUserQueue.Writer.WriteAsync(new()
 | 
			
		||||
            {
 | 
			
		||||
                Action = action,
 | 
			
		||||
@@ -259,6 +275,7 @@ public class ProtectionService : INService
 | 
			
		||||
                MuteTime = muteTime,
 | 
			
		||||
                RoleId = roleId
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ = OnAntiProtectionTriggered(action, pt, gus);
 | 
			
		||||
    }
 | 
			
		||||
@@ -385,18 +402,23 @@ public class ProtectionService : INService
 | 
			
		||||
 | 
			
		||||
    public async Task<bool?> AntiSpamIgnoreAsync(ulong guildId, ulong channelId)
 | 
			
		||||
    {
 | 
			
		||||
        var obj = new AntiSpamIgnore { ChannelId = channelId };
 | 
			
		||||
        var obj = new AntiSpamIgnore
 | 
			
		||||
        {
 | 
			
		||||
            ChannelId = channelId
 | 
			
		||||
        };
 | 
			
		||||
        bool added;
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var gc = uow.GuildConfigsForId(guildId,
 | 
			
		||||
            set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
 | 
			
		||||
        var spam = gc.AntiSpamSetting;
 | 
			
		||||
        if (spam is null) return null;
 | 
			
		||||
        if (spam is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful
 | 
			
		||||
        {
 | 
			
		||||
            if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
 | 
			
		||||
                temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
 | 
			
		||||
 | 
			
		||||
            added = true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -405,6 +427,7 @@ public class ProtectionService : INService
 | 
			
		||||
            uow.Set<AntiSpamIgnore>().Remove(toRemove); // remove from db
 | 
			
		||||
            if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
 | 
			
		||||
                temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache
 | 
			
		||||
 | 
			
		||||
            added = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ public sealed class UserSpamStats
 | 
			
		||||
            lock (_applyLock)
 | 
			
		||||
            {
 | 
			
		||||
                Cleanup();
 | 
			
		||||
                Log.Information("{Count}",_messageTracker.Count.ToString());
 | 
			
		||||
                Log.Information("{Count}", _messageTracker.Count.ToString());
 | 
			
		||||
                return _messageTracker.Count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -40,13 +40,18 @@ public partial class Administration
 | 
			
		||||
                                     }
 | 
			
		||||
 | 
			
		||||
                                     var role = (IRole)roleResult.BestMatch;
 | 
			
		||||
                                     if (role.Position > ((IGuildUser)ctx.User).GetRoles()
 | 
			
		||||
                                                                               .Select(r => r.Position)
 | 
			
		||||
                                                                               .Max()
 | 
			
		||||
                                     if (role.Position
 | 
			
		||||
                                         > ((IGuildUser)ctx.User).GetRoles()
 | 
			
		||||
                                                                 .Select(r => r.Position)
 | 
			
		||||
                                                                 .Max()
 | 
			
		||||
                                         && ctx.User.Id != ctx.Guild.OwnerId)
 | 
			
		||||
                                         return null;
 | 
			
		||||
                                     var emote = x.Last().ToIEmote();
 | 
			
		||||
                                     return new { role, emote };
 | 
			
		||||
                                     return new
 | 
			
		||||
                                     {
 | 
			
		||||
                                         role,
 | 
			
		||||
                                         emote
 | 
			
		||||
                                     };
 | 
			
		||||
                                 })
 | 
			
		||||
                                 .Where(x => x is not null)
 | 
			
		||||
                                 .WhenAll();
 | 
			
		||||
@@ -59,7 +64,10 @@ public partial class Administration
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await target.AddReactionAsync(x.emote,
 | 
			
		||||
                        new() { RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit });
 | 
			
		||||
                        new()
 | 
			
		||||
                        {
 | 
			
		||||
                            RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
                catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.BadRequest)
 | 
			
		||||
                {
 | 
			
		||||
@@ -79,9 +87,10 @@ public partial class Administration
 | 
			
		||||
                        ReactionRoles = all.Select(x =>
 | 
			
		||||
                                           {
 | 
			
		||||
                                               return new ReactionRole
 | 
			
		||||
                                                   {
 | 
			
		||||
                                                       EmoteName = x.emote.ToString(), RoleId = x.role.Id
 | 
			
		||||
                                                   };
 | 
			
		||||
                                               {
 | 
			
		||||
                                                   EmoteName = x.emote.ToString(),
 | 
			
		||||
                                                   RoleId = x.role.Id
 | 
			
		||||
                                               };
 | 
			
		||||
                                           })
 | 
			
		||||
                                           .ToList()
 | 
			
		||||
                    }))
 | 
			
		||||
@@ -144,7 +153,8 @@ public partial class Administration
 | 
			
		||||
                {
 | 
			
		||||
                    var ch = g.GetTextChannel(rr.ChannelId);
 | 
			
		||||
                    IUserMessage msg = null;
 | 
			
		||||
                    if (ch is not null) msg = await ch.GetMessageAsync(rr.MessageId) as IUserMessage;
 | 
			
		||||
                    if (ch is not null)
 | 
			
		||||
                        msg = await ch.GetMessageAsync(rr.MessageId) as IUserMessage;
 | 
			
		||||
                    var content = msg?.Content.TrimTo(30) ?? "DELETED!";
 | 
			
		||||
                    embed.AddField($"**{rr.Index + 1}.** {ch?.Name ?? "DELETED!"}",
 | 
			
		||||
                        GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content)));
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,10 @@ public class RoleCommandsService : INService
 | 
			
		||||
                var dl = await msg.GetOrDownloadAsync();
 | 
			
		||||
                await dl.RemoveReactionAsync(reaction.Emote,
 | 
			
		||||
                    dl.Author,
 | 
			
		||||
                    new() { RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502 });
 | 
			
		||||
                    new()
 | 
			
		||||
                    {
 | 
			
		||||
                        RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
 | 
			
		||||
                    });
 | 
			
		||||
                Log.Warning("User {Author} is adding unrelated reactions to the reaction roles message", dl.Author);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -304,7 +304,7 @@ public partial class Administration
 | 
			
		||||
            var toLeave = _client.Guilds
 | 
			
		||||
                                 .Where(s => s.MemberCount == 1 && s.Users.Count == 1)
 | 
			
		||||
                                 .ToList();
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            foreach (var server in toLeave)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
@@ -425,7 +425,8 @@ public partial class Administration
 | 
			
		||||
        {
 | 
			
		||||
            var success = await _service.SetAvatar(img);
 | 
			
		||||
 | 
			
		||||
            if (success) await ReplyConfirmLocalizedAsync(strs.set_avatar);
 | 
			
		||||
            if (success)
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.set_avatar);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,8 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
 | 
			
		||||
        HandleStatusChanges();
 | 
			
		||||
 | 
			
		||||
        if (_client.ShardId == 0) _pubSub.Sub(_imagesReloadKey, async _ => await _imgs.Reload());
 | 
			
		||||
        if (_client.ShardId == 0)
 | 
			
		||||
            _pubSub.Sub(_imagesReloadKey, async _ => await _imgs.Reload());
 | 
			
		||||
 | 
			
		||||
        _pubSub.Sub(_guildLeaveKey,
 | 
			
		||||
            async input =>
 | 
			
		||||
@@ -70,7 +71,8 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
 | 
			
		||||
                var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr
 | 
			
		||||
                                                                || g.Name.Trim().ToUpperInvariant() == guildStr);
 | 
			
		||||
                if (server is null) return;
 | 
			
		||||
                if (server is null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (server.OwnerId != _client.CurrentUser.Id)
 | 
			
		||||
                {
 | 
			
		||||
@@ -90,15 +92,16 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        autoCommands = uow.AutoCommands.AsNoTracking()
 | 
			
		||||
                           .Where(x => x.Interval >= 5)
 | 
			
		||||
                           .AsEnumerable()
 | 
			
		||||
                           .GroupBy(x => x.GuildId)
 | 
			
		||||
                           .ToDictionary(x => x.Key,
 | 
			
		||||
                               y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
 | 
			
		||||
                           .ToConcurrent();
 | 
			
		||||
                          .Where(x => x.Interval >= 5)
 | 
			
		||||
                          .AsEnumerable()
 | 
			
		||||
                          .GroupBy(x => x.GuildId)
 | 
			
		||||
                          .ToDictionary(x => x.Key,
 | 
			
		||||
                              y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
 | 
			
		||||
                          .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
 | 
			
		||||
        foreach (var cmd in startupCommands)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await ExecuteCommand(cmd);
 | 
			
		||||
@@ -106,8 +109,10 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_client.ShardId == 0) await LoadOwnerChannels();
 | 
			
		||||
        if (_client.ShardId == 0)
 | 
			
		||||
            await LoadOwnerChannels();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Timer TimerFromAutoCommand(AutoCommand x)
 | 
			
		||||
@@ -171,13 +176,14 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
    private async Task LoadOwnerChannels()
 | 
			
		||||
    {
 | 
			
		||||
        var channels = await _creds.OwnerIds.Select(id =>
 | 
			
		||||
        {
 | 
			
		||||
            var user = _client.GetUser(id);
 | 
			
		||||
            if (user is null)
 | 
			
		||||
                return Task.FromResult<IDMChannel>(null);
 | 
			
		||||
                                   {
 | 
			
		||||
                                       var user = _client.GetUser(id);
 | 
			
		||||
                                       if (user is null)
 | 
			
		||||
                                           return Task.FromResult<IDMChannel>(null);
 | 
			
		||||
 | 
			
		||||
            return user.CreateDMChannelAsync();
 | 
			
		||||
        }).WhenAll();
 | 
			
		||||
                                       return user.CreateDMChannelAsync();
 | 
			
		||||
                                   })
 | 
			
		||||
                                   .WhenAll();
 | 
			
		||||
 | 
			
		||||
        ownerChannels = channels.Where(x => x is not null)
 | 
			
		||||
                                .ToDictionary(x => x.Recipient.Id, x => x)
 | 
			
		||||
@@ -187,7 +193,9 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
            Log.Warning(
 | 
			
		||||
                "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server");
 | 
			
		||||
        else
 | 
			
		||||
            Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels", ownerChannels.Count, _creds.OwnerIds.Count);
 | 
			
		||||
            Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels",
 | 
			
		||||
                ownerChannels.Count,
 | 
			
		||||
                _creds.OwnerIds.Count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LeaveGuild(string guildStr)
 | 
			
		||||
@@ -214,6 +222,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
                var allOwnerChannels = ownerChannels.Values;
 | 
			
		||||
 | 
			
		||||
                foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        await ownerCh.SendConfirmAsync(_eb, title, toSend);
 | 
			
		||||
@@ -222,6 +231,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
@@ -337,10 +347,22 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public Task SetGameAsync(string game, ActivityType type)
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey, new() { Name = game, Link = null, Type = type });
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey,
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = game,
 | 
			
		||||
                Link = null,
 | 
			
		||||
                Type = type
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public Task SetStreamAsync(string name, string link)
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey, new() { Name = name, Link = link, Type = ActivityType.Streaming });
 | 
			
		||||
        => _pubSub.Pub(_activitySetKey,
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = name,
 | 
			
		||||
                Link = link,
 | 
			
		||||
                Type = ActivityType.Streaming
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    private sealed class ActivityPubData
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -112,6 +112,7 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
                        rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫");
 | 
			
		||||
                        foreach (var (model, role) in kvp.AsEnumerable())
 | 
			
		||||
                        {
 | 
			
		||||
                            if (role is null)
 | 
			
		||||
                            {
 | 
			
		||||
                            }
 | 
			
		||||
@@ -123,6 +124,7 @@ public partial class Administration
 | 
			
		||||
                                else
 | 
			
		||||
                                    rolesStr.AppendLine("   " + role.Name + $" (lvl {model.LevelRequirement}+)");
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        rolesStr.AppendLine();
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,15 @@ public class SelfAssignedRolesService : INService
 | 
			
		||||
    {
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var roles = uow.SelfAssignableRoles.GetFromGuild(guildId);
 | 
			
		||||
        if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) return false;
 | 
			
		||||
        if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        uow.SelfAssignableRoles.Add(new() { Group = group, RoleId = role.Id, GuildId = role.Guild.Id });
 | 
			
		||||
        uow.SelfAssignableRoles.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            Group = group,
 | 
			
		||||
            RoleId = role.Id,
 | 
			
		||||
            GuildId = role.Guild.Id
 | 
			
		||||
        });
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -67,7 +73,8 @@ public class SelfAssignedRolesService : INService
 | 
			
		||||
            return (AssignResult.ErrNotAssignable, autoDelete, null);
 | 
			
		||||
        if (theRoleYouWant.LevelRequirement > userLevelData.Level)
 | 
			
		||||
            return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement);
 | 
			
		||||
        if (guildUser.RoleIds.Contains(role.Id)) return (AssignResult.ErrAlreadyHave, autoDelete, null);
 | 
			
		||||
        if (guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            return (AssignResult.ErrAlreadyHave, autoDelete, null);
 | 
			
		||||
 | 
			
		||||
        var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray();
 | 
			
		||||
        if (exclusive)
 | 
			
		||||
@@ -116,7 +123,11 @@ public class SelfAssignedRolesService : INService
 | 
			
		||||
        }
 | 
			
		||||
        else if (toUpdate is null)
 | 
			
		||||
        {
 | 
			
		||||
            gc.SelfAssignableRoleGroupNames.Add(new() { Name = name, Number = group });
 | 
			
		||||
            gc.SelfAssignableRoleGroupNames.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = name,
 | 
			
		||||
                Number = group
 | 
			
		||||
            });
 | 
			
		||||
            set = true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -136,7 +147,8 @@ public class SelfAssignedRolesService : INService
 | 
			
		||||
 | 
			
		||||
        if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null)
 | 
			
		||||
            return (RemoveResult.ErrNotAssignable, autoDelete);
 | 
			
		||||
        if (!guildUser.RoleIds.Contains(role.Id)) return (RemoveResult.ErrNotHave, autoDelete);
 | 
			
		||||
        if (!guildUser.RoleIds.Contains(role.Id))
 | 
			
		||||
            return (RemoveResult.ErrNotHave, autoDelete);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await guildUser.RemoveRoleAsync(role);
 | 
			
		||||
@@ -198,7 +210,8 @@ public class SelfAssignedRolesService : INService
 | 
			
		||||
        return areExclusive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string> GroupNames
 | 
			
		||||
    public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string>
 | 
			
		||||
        GroupNames
 | 
			
		||||
        ) GetRoles(IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        var exclusive = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,11 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
        if (removed == 0)
 | 
			
		||||
        {
 | 
			
		||||
            var toAdd = new IgnoredLogItem { LogItemId = itemId, ItemType = itemType };
 | 
			
		||||
            var toAdd = new IgnoredLogItem
 | 
			
		||||
            {
 | 
			
		||||
                LogItemId = itemId,
 | 
			
		||||
                ItemType = itemType
 | 
			
		||||
            };
 | 
			
		||||
            logSetting.LogIgnores.Add(toAdd);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -183,7 +187,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserUpdated(SocketUser before, SocketUser uAfter)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -306,7 +310,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -351,7 +355,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType muteType,
 | 
			
		||||
        string reason)
 | 
			
		||||
        => _= Task.Run(async () =>
 | 
			
		||||
        => _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -396,7 +400,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        IUser mod,
 | 
			
		||||
        MuteType muteType,
 | 
			
		||||
        string reason)
 | 
			
		||||
        => _= Task.Run(async () =>
 | 
			
		||||
        => _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -443,7 +447,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    public Task TriggeredAntiProtection(PunishmentAction action, ProtectionType protection, params IGuildUser[] users)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -453,7 +457,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                if (!GuildLogSettings.TryGetValue(users.First().Guild.Id, out var logSetting)
 | 
			
		||||
                    || logSetting.LogOtherId is null)
 | 
			
		||||
                    return;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                ITextChannel? logChannel;
 | 
			
		||||
                if ((logChannel = await TryGetLogChannel(users.First().Guild, logSetting, LogType.Other)) is null)
 | 
			
		||||
                    return;
 | 
			
		||||
@@ -513,7 +517,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_GuildUserUpdated(Cacheable<SocketGuildUser, ulong> optBefore, SocketGuildUser after)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -575,7 +579,8 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
                if (!before.IsBot
 | 
			
		||||
                    && logSetting.LogUserPresenceId is not null
 | 
			
		||||
                    && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserPresence)) is not null)
 | 
			
		||||
                    && (logChannel =
 | 
			
		||||
                        await TryGetLogChannel(before.Guild, logSetting, LogType.UserPresence)) is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (before.Status != after.Status)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -585,7 +590,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                                      strs.user_status_change("👤" + Format.Bold(after.Username),
 | 
			
		||||
                                          Format.Bold(after.Status.ToString())));
 | 
			
		||||
                        PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                            new List<string> { str },
 | 
			
		||||
                            new List<string>
 | 
			
		||||
                            {
 | 
			
		||||
                                str
 | 
			
		||||
                            },
 | 
			
		||||
                            (_, list) =>
 | 
			
		||||
                            {
 | 
			
		||||
                                list.Add(str);
 | 
			
		||||
@@ -597,7 +605,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                        var str =
 | 
			
		||||
                            $"👾`{PrettyCurrentTime(after.Guild)}`👤__**{after.Username}**__ is now playing **{after.Activities.FirstOrDefault()?.Name ?? "-"}**.";
 | 
			
		||||
                        PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                            new List<string> { str },
 | 
			
		||||
                            new List<string>
 | 
			
		||||
                            {
 | 
			
		||||
                                str
 | 
			
		||||
                            },
 | 
			
		||||
                            (_, list) =>
 | 
			
		||||
                            {
 | 
			
		||||
                                list.Add(str);
 | 
			
		||||
@@ -616,7 +627,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_ChannelUpdated(IChannel cbefore, IChannel cafter)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -664,7 +675,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_ChannelDestroyed(IChannel ich)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -702,7 +713,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_ChannelCreated(IChannel ich)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -738,7 +749,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState before, SocketVoiceState after)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -784,7 +795,10 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(str))
 | 
			
		||||
                    PresenceUpdates.AddOrUpdate(logChannel,
 | 
			
		||||
                        new List<string> { str },
 | 
			
		||||
                        new List<string>
 | 
			
		||||
                        {
 | 
			
		||||
                            str
 | 
			
		||||
                        },
 | 
			
		||||
                        (_, list) =>
 | 
			
		||||
                        {
 | 
			
		||||
                            list.Add(str);
 | 
			
		||||
@@ -801,7 +815,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserLeft(SocketGuild guild, SocketUser usr)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -836,7 +850,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserJoined(IGuildUser usr)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -875,7 +889,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserUnbanned(IUser usr, IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -910,7 +924,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_UserBanned(IUser usr, IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -947,7 +961,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    private Task _client_MessageDeleted(Cacheable<IMessage, ulong> optMsg, Cacheable<IMessageChannel, ulong> optCh)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -1001,7 +1015,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
        SocketMessage imsg2,
 | 
			
		||||
        ISocketMessageChannel ch)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,8 @@ public partial class Administration
 | 
			
		||||
                Log.Warning(ex, "Exception occured while warning a user");
 | 
			
		||||
                var errorEmbed = _eb.Create().WithErrorColor().WithDescription(GetText(strs.cant_apply_punishment));
 | 
			
		||||
 | 
			
		||||
                if (dmFailed) errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
                if (dmFailed)
 | 
			
		||||
                    errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
                await ctx.Channel.EmbedAsync(errorEmbed);
 | 
			
		||||
                return;
 | 
			
		||||
@@ -102,7 +103,8 @@ public partial class Administration
 | 
			
		||||
                embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()),
 | 
			
		||||
                    Format.Bold(punishment.Punishment.ToString()))));
 | 
			
		||||
 | 
			
		||||
            if (dmFailed) embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
@@ -359,7 +361,8 @@ public partial class Administration
 | 
			
		||||
        [UserPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async partial Task WarnPunish(int number)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.WarnPunishRemove(ctx.Guild.Id, number)) return;
 | 
			
		||||
            if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.warn_punish_rem(Format.Bold(number.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
@@ -402,7 +405,8 @@ public partial class Administration
 | 
			
		||||
                {
 | 
			
		||||
                    var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
 | 
			
		||||
                    var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
 | 
			
		||||
                    if (embed is not null) await guildUser.SendAsync(embed);
 | 
			
		||||
                    if (embed is not null)
 | 
			
		||||
                        await guildUser.SendAsync(embed);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
@@ -419,7 +423,8 @@ public partial class Administration
 | 
			
		||||
                                time.Time.Humanize(3, minUnit: TimeUnit.Minute, culture: Culture),
 | 
			
		||||
                                true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed) toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
@@ -463,7 +468,8 @@ public partial class Administration
 | 
			
		||||
            {
 | 
			
		||||
                var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
 | 
			
		||||
                var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
 | 
			
		||||
                if (embed is not null) await ctx.User.SendAsync(embed);
 | 
			
		||||
                if (embed is not null)
 | 
			
		||||
                    await ctx.User.SendAsync(embed);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
@@ -478,7 +484,8 @@ public partial class Administration
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed) toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
@@ -648,7 +655,8 @@ public partial class Administration
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed) toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
@@ -699,7 +707,8 @@ public partial class Administration
 | 
			
		||||
                            .AddField(GetText(strs.username), user.ToString(), true)
 | 
			
		||||
                            .AddField("ID", user.Id.ToString(), true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed) toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
                toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
        }
 | 
			
		||||
@@ -719,6 +728,7 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
            foreach (var userStr in userStrings)
 | 
			
		||||
            {
 | 
			
		||||
                if (ulong.TryParse(userStr, out var userId))
 | 
			
		||||
                {
 | 
			
		||||
                    IUser user = await ctx.Guild.GetUserAsync(userId)
 | 
			
		||||
@@ -739,7 +749,8 @@ public partial class Administration
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    //Hierachy checks only if the user is in the guild
 | 
			
		||||
                    if (user is IGuildUser gu && !await CheckRoleHierarchy(gu)) return;
 | 
			
		||||
                    if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    banning.Add(user);
 | 
			
		||||
                }
 | 
			
		||||
@@ -747,6 +758,7 @@ public partial class Administration
 | 
			
		||||
                {
 | 
			
		||||
                    missing.Add(userStr);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var missStr = string.Join("\n", missing);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(missStr))
 | 
			
		||||
@@ -760,6 +772,7 @@ public partial class Administration
 | 
			
		||||
            var banningMessage = await ctx.Channel.EmbedAsync(toSend);
 | 
			
		||||
 | 
			
		||||
            foreach (var toBan in banning)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban");
 | 
			
		||||
@@ -768,6 +781,7 @@ public partial class Administration
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "Error banning {User} user in {GuildId} server", toBan.Id, ctx.Guild.Id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await banningMessage.ModifyAsync(x => x.Embed = _eb.Create()
 | 
			
		||||
                                                               .WithDescription(
 | 
			
		||||
@@ -805,7 +819,10 @@ public partial class Administration
 | 
			
		||||
                                   .Select(x => ctx.Guild.AddBanAsync(x.Id.Value,
 | 
			
		||||
                                       7,
 | 
			
		||||
                                       x.Reason,
 | 
			
		||||
                                       new() { RetryMode = RetryMode.AlwaysRetry })));
 | 
			
		||||
                                       new()
 | 
			
		||||
                                       {
 | 
			
		||||
                                           RetryMode = RetryMode.AlwaysRetry
 | 
			
		||||
                                       })));
 | 
			
		||||
 | 
			
		||||
            //wait for the message and edit it
 | 
			
		||||
            var banningMessage = await banningMessageTask;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,12 +30,12 @@ public class UserPunishService : INService, IReadyExecutor
 | 
			
		||||
        _bcs = bcs;
 | 
			
		||||
        _client = client;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_client.ShardId != 0)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        var expiryTimer = new PeriodicTimer(TimeSpan.FromHours(12));
 | 
			
		||||
        do
 | 
			
		||||
        {
 | 
			
		||||
@@ -429,7 +429,11 @@ WHERE GuildId={guildId}
 | 
			
		||||
        }
 | 
			
		||||
        else if (template is null)
 | 
			
		||||
        {
 | 
			
		||||
            uow.BanTemplates.Add(new() { GuildId = guildId, Text = text });
 | 
			
		||||
            uow.BanTemplates.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Text = text
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -485,7 +489,8 @@ WHERE GuildId={guildId}
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(template))
 | 
			
		||||
            template = JsonConvert.SerializeObject(new
 | 
			
		||||
            {
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8, description = defaultMessage
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8,
 | 
			
		||||
                description = defaultMessage
 | 
			
		||||
            });
 | 
			
		||||
        // if template is set to "-" do not dm the user
 | 
			
		||||
        else if (template == "-")
 | 
			
		||||
@@ -495,7 +500,8 @@ WHERE GuildId={guildId}
 | 
			
		||||
        else if (!SmartText.CreateFrom(template).IsEmbed)
 | 
			
		||||
            template = JsonConvert.SerializeObject(new
 | 
			
		||||
            {
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8, description = template
 | 
			
		||||
                color = _bcs.Data.Color.Error.PackedValue >> 8,
 | 
			
		||||
                description = template
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        var output = SmartText.CreateFrom(template);
 | 
			
		||||
 
 | 
			
		||||
@@ -49,11 +49,13 @@ public class VcRoleService : INService
 | 
			
		||||
                            {
 | 
			
		||||
                                if (add)
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (!user.RoleIds.Contains(role.Id)) await user.AddRoleAsync(role);
 | 
			
		||||
                                    if (!user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                        await user.AddRoleAsync(role);
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    if (user.RoleIds.Contains(role.Id)) await user.RemoveRoleAsync(role);
 | 
			
		||||
                                    if (user.RoleIds.Contains(role.Id))
 | 
			
		||||
                                        await user.RemoveRoleAsync(role);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch
 | 
			
		||||
@@ -80,7 +82,7 @@ public class VcRoleService : INService
 | 
			
		||||
        using (var uow = _db.GetDbContext())
 | 
			
		||||
        {
 | 
			
		||||
            var configWithVcRole = uow.GuildConfigsForId(arg.GuildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
            _= InitializeVcRole(configWithVcRole);
 | 
			
		||||
            _ = InitializeVcRole(configWithVcRole);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
@@ -137,8 +139,13 @@ public class VcRoleService : INService
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos));
 | 
			
		||||
        var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one
 | 
			
		||||
        if (toDelete is not null) uow.Remove(toDelete);
 | 
			
		||||
        conf.VcRoleInfos.Add(new() { VoiceChannelId = vcId, RoleId = role.Id }); // add new one
 | 
			
		||||
        if (toDelete is not null)
 | 
			
		||||
            uow.Remove(toDelete);
 | 
			
		||||
        conf.VcRoleInfos.Add(new()
 | 
			
		||||
        {
 | 
			
		||||
            VoiceChannelId = vcId,
 | 
			
		||||
            RoleId = role.Id
 | 
			
		||||
        }); // add new one
 | 
			
		||||
        uow.SaveChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -166,7 +173,7 @@ public class VcRoleService : INService
 | 
			
		||||
 | 
			
		||||
        var oldVc = oldState.VoiceChannel;
 | 
			
		||||
        var newVc = newState.VoiceChannel;
 | 
			
		||||
        _= Task.Run(() =>
 | 
			
		||||
        _ = Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -181,7 +188,8 @@ public class VcRoleService : INService
 | 
			
		||||
                        if (oldVc is not null && guildVcRoles.TryGetValue(oldVc.Id, out var role))
 | 
			
		||||
                            Assign(false, gusr, role);
 | 
			
		||||
                        //add new
 | 
			
		||||
                        if (newVc is not null && guildVcRoles.TryGetValue(newVc.Id, out role)) Assign(true, gusr, role);
 | 
			
		||||
                        if (newVc is not null && guildVcRoles.TryGetValue(newVc.Id, out role))
 | 
			
		||||
                            Assign(true, gusr, role);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -92,16 +92,16 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
 | 
			
		||||
            curPage =>
 | 
			
		||||
            {
 | 
			
		||||
                var desc = expressions.OrderBy(ex => ex.Trigger)
 | 
			
		||||
                                          .Skip(curPage * 20)
 | 
			
		||||
                                          .Take(20)
 | 
			
		||||
                                          .Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}"
 | 
			
		||||
                                                        + $"{(ex.DmResponse ? "✉" : "◾")}"
 | 
			
		||||
                                                        + $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}"
 | 
			
		||||
                                                        + $"`{(kwum)ex.Id}` {ex.Trigger}"
 | 
			
		||||
                                                        + (string.IsNullOrWhiteSpace(ex.Reactions)
 | 
			
		||||
                                                            ? string.Empty
 | 
			
		||||
                                                            : " // " + string.Join(" ", ex.GetReactions())))
 | 
			
		||||
                                          .Join('\n');
 | 
			
		||||
                                      .Skip(curPage * 20)
 | 
			
		||||
                                      .Take(20)
 | 
			
		||||
                                      .Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}"
 | 
			
		||||
                                                    + $"{(ex.DmResponse ? "✉" : "◾")}"
 | 
			
		||||
                                                    + $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}"
 | 
			
		||||
                                                    + $"`{(kwum)ex.Id}` {ex.Trigger}"
 | 
			
		||||
                                                    + (string.IsNullOrWhiteSpace(ex.Reactions)
 | 
			
		||||
                                                        ? string.Empty
 | 
			
		||||
                                                        : " // " + string.Join(" ", ex.GetReactions())))
 | 
			
		||||
                                      .Join('\n');
 | 
			
		||||
 | 
			
		||||
                return _eb.Create().WithOkColor().WithTitle(GetText(strs.custom_reactions)).WithDescription(desc);
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -113,14 +113,14 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
                                  .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        newGuildReactions = guildItems.GroupBy(k => k.GuildId!.Value)
 | 
			
		||||
                                       .ToDictionary(g => g.Key,
 | 
			
		||||
                                           g => g.Select(x =>
 | 
			
		||||
                                                 {
 | 
			
		||||
                                                     x.Trigger = x.Trigger.Replace(MENTION_PH, _bot.Mention);
 | 
			
		||||
                                                     return x;
 | 
			
		||||
                                                 })
 | 
			
		||||
                                                 .ToArray())
 | 
			
		||||
                                       .ToConcurrent();
 | 
			
		||||
                                      .ToDictionary(g => g.Key,
 | 
			
		||||
                                          g => g.Select(x =>
 | 
			
		||||
                                                {
 | 
			
		||||
                                                    x.Trigger = x.Trigger.Replace(MENTION_PH, _bot.Mention);
 | 
			
		||||
                                                    return x;
 | 
			
		||||
                                                })
 | 
			
		||||
                                                .ToArray())
 | 
			
		||||
                                      .ToConcurrent();
 | 
			
		||||
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
@@ -180,7 +180,8 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
                    var wp = content.GetWordPosition(trigger);
 | 
			
		||||
 | 
			
		||||
                    // if it is, then that's valid
 | 
			
		||||
                    if (wp != WordPosition.None) result.Add(expr);
 | 
			
		||||
                    if (wp != WordPosition.None)
 | 
			
		||||
                        result.Add(expr);
 | 
			
		||||
 | 
			
		||||
                    // if it's not, then it cant' work under any circumstance,
 | 
			
		||||
                    // because content is greater than the trigger length
 | 
			
		||||
@@ -204,7 +205,8 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
            {
 | 
			
		||||
                // if input length is the same as trigger length
 | 
			
		||||
                // reaction can only trigger if the strings are equal
 | 
			
		||||
                if (content.SequenceEqual(expr.Trigger)) result.Add(expr);
 | 
			
		||||
                if (content.SequenceEqual(expr.Trigger))
 | 
			
		||||
                    result.Add(expr);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -345,8 +347,11 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
                {
 | 
			
		||||
                    var newArray = old.ToArray();
 | 
			
		||||
                    for (var i = 0; i < newArray.Length; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (newArray[i].Id == expr.Id)
 | 
			
		||||
                            newArray[i] = expr;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return newArray;
 | 
			
		||||
                });
 | 
			
		||||
        else
 | 
			
		||||
@@ -354,8 +359,10 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
            {
 | 
			
		||||
                var exprs = globalReactions;
 | 
			
		||||
                for (var i = 0; i < exprs.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (exprs[i].Id == expr.Id)
 | 
			
		||||
                        exprs[i] = expr;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -386,7 +393,8 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
            var expr = Array.Find(globalReactions, item => item.Id == id);
 | 
			
		||||
            if (expr is not null) return _pubSub.Pub(_gexprDeletedkey, expr.Id);
 | 
			
		||||
            if (expr is not null)
 | 
			
		||||
                return _pubSub.Pub(_gexprDeletedkey, expr.Id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
@@ -516,17 +524,17 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        {
 | 
			
		||||
            var trigger = entry.Key;
 | 
			
		||||
            await uow.Expressions.AddRangeAsync(entry.Value.Where(expr => !string.IsNullOrWhiteSpace(expr.Res))
 | 
			
		||||
                                                         .Select(expr => new NadekoExpression
 | 
			
		||||
                                                         {
 | 
			
		||||
                                                             GuildId = guildId,
 | 
			
		||||
                                                             Response = expr.Res,
 | 
			
		||||
                                                             Reactions = expr.React?.Join("@@@"),
 | 
			
		||||
                                                             Trigger = trigger,
 | 
			
		||||
                                                             AllowTarget = expr.At,
 | 
			
		||||
                                                             ContainsAnywhere = expr.Ca,
 | 
			
		||||
                                                             DmResponse = expr.Dm,
 | 
			
		||||
                                                             AutoDeleteTrigger = expr.Ad
 | 
			
		||||
                                                         }));
 | 
			
		||||
                                                     .Select(expr => new NadekoExpression
 | 
			
		||||
                                                     {
 | 
			
		||||
                                                         GuildId = guildId,
 | 
			
		||||
                                                         Response = expr.Res,
 | 
			
		||||
                                                         Reactions = expr.React?.Join("@@@"),
 | 
			
		||||
                                                         Trigger = trigger,
 | 
			
		||||
                                                         AllowTarget = expr.At,
 | 
			
		||||
                                                         ContainsAnywhere = expr.Ca,
 | 
			
		||||
                                                         DmResponse = expr.Dm,
 | 
			
		||||
                                                         AutoDeleteTrigger = expr.Ad
 | 
			
		||||
                                                     }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
@@ -560,11 +568,13 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        lock (_gexprWriteLock)
 | 
			
		||||
        {
 | 
			
		||||
            for (var i = 0; i < globalReactions.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (globalReactions[i].Id == c.Id)
 | 
			
		||||
                {
 | 
			
		||||
                    globalReactions[i] = c;
 | 
			
		||||
                    return default;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if edited expr is not found?!
 | 
			
		||||
            // add it
 | 
			
		||||
@@ -614,7 +624,12 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
    public async Task<NadekoExpression> AddAsync(ulong? guildId, string key, string message)
 | 
			
		||||
    {
 | 
			
		||||
        key = key.ToLowerInvariant();
 | 
			
		||||
        var expr = new NadekoExpression { GuildId = guildId, Trigger = key, Response = message };
 | 
			
		||||
        var expr = new NadekoExpression
 | 
			
		||||
        {
 | 
			
		||||
            GuildId = guildId,
 | 
			
		||||
            Trigger = key,
 | 
			
		||||
            Response = message
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (expr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            expr.AllowTarget = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -99,8 +99,10 @@ public sealed class AnimalRace : IDisposable
 | 
			
		||||
        if (_users.Count <= 1)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var user in _users)
 | 
			
		||||
            {
 | 
			
		||||
                if (user.Bet > 0)
 | 
			
		||||
                    await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund"));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = OnStartingFailed?.Invoke(this);
 | 
			
		||||
            CurrentPhase = Phase.Ended;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
            Task ClientMessageReceived(SocketMessage arg)
 | 
			
		||||
            {
 | 
			
		||||
                _= Task.Run(() =>
 | 
			
		||||
                _ = Task.Run(() =>
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,9 +75,7 @@ public partial class Gambling
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (msg is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    _= msg.DeleteAsync();
 | 
			
		||||
                }
 | 
			
		||||
                    _ = msg.DeleteAsync();
 | 
			
		||||
 | 
			
		||||
                var c = bj.Dealer.Cards.Select(x => x.GetEmojiString())
 | 
			
		||||
                          .ToList();
 | 
			
		||||
@@ -99,7 +97,8 @@ public partial class Gambling
 | 
			
		||||
                               .WithTitle("BlackJack")
 | 
			
		||||
                               .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);
 | 
			
		||||
 | 
			
		||||
                if (bj.CurrentUser is not null) embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser}");
 | 
			
		||||
                if (bj.CurrentUser is not null)
 | 
			
		||||
                    embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser}");
 | 
			
		||||
 | 
			
		||||
                foreach (var p in bj.Players)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ public class Blackjack
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Start()
 | 
			
		||||
        => _= GameLoop();
 | 
			
		||||
        => _ = GameLoop();
 | 
			
		||||
 | 
			
		||||
    public async Task GameLoop()
 | 
			
		||||
    {
 | 
			
		||||
@@ -73,11 +73,13 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
            //go through all users and ask them what they want to do
 | 
			
		||||
            foreach (var usr in Players.Where(x => !x.Done))
 | 
			
		||||
            {
 | 
			
		||||
                while (!usr.Done)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information("Waiting for {DiscordUser}'s move", usr.DiscordUser);
 | 
			
		||||
                    await PromptUserMove(usr);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await PrintState();
 | 
			
		||||
            State = GameState.Ended;
 | 
			
		||||
@@ -85,13 +87,13 @@ public class Blackjack
 | 
			
		||||
            Log.Information("Dealer moves");
 | 
			
		||||
            await DealerMoves();
 | 
			
		||||
            await PrintState();
 | 
			
		||||
            _= GameEnded?.Invoke(this);
 | 
			
		||||
            _ = GameEnded?.Invoke(this);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE");
 | 
			
		||||
            State = GameState.Ended;
 | 
			
		||||
            _= GameEnded?.Invoke(this);
 | 
			
		||||
            _ = GameEnded?.Invoke(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -106,14 +108,10 @@ public class Blackjack
 | 
			
		||||
        // if he doesn't - stand
 | 
			
		||||
        var finished = await Task.WhenAny(pause, currentUserMove.Task);
 | 
			
		||||
        if (finished == pause)
 | 
			
		||||
        {
 | 
			
		||||
            await Stand(usr);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            cts.Cancel();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        CurrentUser = null;
 | 
			
		||||
        currentUserMove = null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -129,10 +127,11 @@ public class Blackjack
 | 
			
		||||
            if (Players.Count >= 5)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            if (!await _cs.RemoveAsync(user, bet, new("blackjack","gamble"))) return false;
 | 
			
		||||
            if (!await _cs.RemoveAsync(user, bet, new("blackjack", "gamble")))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            Players.Add(new(user, bet));
 | 
			
		||||
            _= PrintState();
 | 
			
		||||
            _ = PrintState();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
@@ -204,22 +203,28 @@ public class Blackjack
 | 
			
		||||
 | 
			
		||||
        if (hw > 21)
 | 
			
		||||
            foreach (var usr in Players)
 | 
			
		||||
            {
 | 
			
		||||
                if (usr.State is User.UserState.Stand or User.UserState.Blackjack)
 | 
			
		||||
                    usr.State = User.UserState.Won;
 | 
			
		||||
                else
 | 
			
		||||
                    usr.State = User.UserState.Lost;
 | 
			
		||||
            }
 | 
			
		||||
        else
 | 
			
		||||
            foreach (var usr in Players)
 | 
			
		||||
            {
 | 
			
		||||
                if (usr.State == User.UserState.Blackjack)
 | 
			
		||||
                    usr.State = User.UserState.Won;
 | 
			
		||||
                else if (usr.State == User.UserState.Stand)
 | 
			
		||||
                    usr.State = hw < usr.GetHandValue() ? User.UserState.Won : User.UserState.Lost;
 | 
			
		||||
                else
 | 
			
		||||
                    usr.State = User.UserState.Lost;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        foreach (var usr in Players)
 | 
			
		||||
        {
 | 
			
		||||
            if (usr.State is User.UserState.Won or User.UserState.Blackjack)
 | 
			
		||||
                await _cs.AddAsync(usr.DiscordUser.Id, usr.Bet * 2, new("blackjack", "win"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Double(IUser u)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ public abstract class Player
 | 
			
		||||
        // reduce the value by 10 until it drops below 22
 | 
			
		||||
        // (emulating the fact that ace is either a 1 or a 11)
 | 
			
		||||
        var i = Cards.Count(x => x.Number == 1);
 | 
			
		||||
        while (val > 21 && i-- > 0) val -= 10;
 | 
			
		||||
        while (val > 21 && i-- > 0)
 | 
			
		||||
            val -= 10;
 | 
			
		||||
        return val;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -81,14 +81,15 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
        _cs = cs;
 | 
			
		||||
 | 
			
		||||
        _rng = new();
 | 
			
		||||
        for (var i = 0; i < NUMBER_OF_COLUMNS * NUMBER_OF_ROWS; i++) _gameState[i] = Field.Empty;
 | 
			
		||||
        for (var i = 0; i < NUMBER_OF_COLUMNS * NUMBER_OF_ROWS; i++)
 | 
			
		||||
            _gameState[i] = Field.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Initialize()
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentPhase != Phase.Joining)
 | 
			
		||||
            return;
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Delay(15000);
 | 
			
		||||
            await _locker.WaitAsync();
 | 
			
		||||
@@ -173,11 +174,13 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
 | 
			
		||||
            var start = NUMBER_OF_ROWS * inputCol;
 | 
			
		||||
            for (var i = start; i < start + NUMBER_OF_ROWS; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (_gameState[i] == Field.Empty)
 | 
			
		||||
                {
 | 
			
		||||
                    _gameState[i] = GetPlayerPiece(userId);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //check winnning condition
 | 
			
		||||
            // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected
 | 
			
		||||
@@ -233,7 +236,8 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
                                    EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
 | 
			
		||||
                                else
 | 
			
		||||
                                    continue;
 | 
			
		||||
                            else break;
 | 
			
		||||
                            else
 | 
			
		||||
                                break;
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -271,7 +275,8 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
                            var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)];
 | 
			
		||||
                            if (cur == first)
 | 
			
		||||
                                same++;
 | 
			
		||||
                            else break;
 | 
			
		||||
                            else
 | 
			
		||||
                                break;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (same == 4)
 | 
			
		||||
@@ -298,7 +303,8 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
                            var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)];
 | 
			
		||||
                            if (cur == first)
 | 
			
		||||
                                same++;
 | 
			
		||||
                            else break;
 | 
			
		||||
                            else
 | 
			
		||||
                                break;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (same == 4)
 | 
			
		||||
@@ -311,7 +317,8 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //check draw? if it's even possible
 | 
			
		||||
            if (_gameState.All(x => x != Field.Empty)) EndGame(Result.Draw, null);
 | 
			
		||||
            if (_gameState.All(x => x != Field.Empty))
 | 
			
		||||
                EndGame(Result.Draw, null);
 | 
			
		||||
 | 
			
		||||
            if (CurrentPhase != Phase.Ended)
 | 
			
		||||
            {
 | 
			
		||||
@@ -323,7 +330,7 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
                ResetTimer();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _= OnGameStateUpdated?.Invoke(this);
 | 
			
		||||
            _ = OnGameStateUpdated?.Invoke(this);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally { _locker.Release(); }
 | 
			
		||||
@@ -337,7 +344,7 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentPhase == Phase.Ended)
 | 
			
		||||
            return;
 | 
			
		||||
        _= OnGameEnded?.Invoke(this, result);
 | 
			
		||||
        _ = OnGameEnded?.Invoke(this, result);
 | 
			
		||||
        CurrentPhase = Phase.Ended;
 | 
			
		||||
 | 
			
		||||
        if (result == Result.Draw)
 | 
			
		||||
@@ -359,8 +366,11 @@ public sealed class Connect4Game : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        var start = NUMBER_OF_ROWS * column;
 | 
			
		||||
        for (var i = start; i < start + NUMBER_OF_ROWS; i++)
 | 
			
		||||
        {
 | 
			
		||||
            if (_gameState[i] == Field.Empty)
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,8 @@ public partial class Gambling
 | 
			
		||||
            {
 | 
			
		||||
                if (value is < 0 or > 7)
 | 
			
		||||
                    repostCounter = 0;
 | 
			
		||||
                else repostCounter = value;
 | 
			
		||||
                else
 | 
			
		||||
                    repostCounter = value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -88,10 +89,11 @@ public partial class Gambling
 | 
			
		||||
                if (ctx.Channel.Id != arg.Channel.Id)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                _= Task.Run(async () =>
 | 
			
		||||
                _ = Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    var success = false;
 | 
			
		||||
                    if (int.TryParse(arg.Content, out var col)) success = await game.Input(arg.Author.Id, col);
 | 
			
		||||
                    if (int.TryParse(arg.Content, out var col))
 | 
			
		||||
                        success = await game.Input(arg.Author.Id, col);
 | 
			
		||||
 | 
			
		||||
                    if (success)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -100,7 +102,8 @@ public partial class Gambling
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        if (game.CurrentPhase is Connect4Game.Phase.Joining or Connect4Game.Phase.Ended) return;
 | 
			
		||||
                        if (game.CurrentPhase is Connect4Game.Phase.Joining or Connect4Game.Phase.Ended)
 | 
			
		||||
                            return;
 | 
			
		||||
                        RepostCounter++;
 | 
			
		||||
                        if (RepostCounter == 0)
 | 
			
		||||
                            try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); }
 | 
			
		||||
@@ -185,7 +188,8 @@ public partial class Gambling
 | 
			
		||||
                sb.AppendLine();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (var i = 0; i < Connect4Game.NUMBER_OF_COLUMNS; i++) sb.Append(_numbers[i]);
 | 
			
		||||
            for (var i = 0; i < Connect4Game.NUMBER_OF_COLUMNS; i++)
 | 
			
		||||
                sb.Append(_numbers[i]);
 | 
			
		||||
            return sb.ToString();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -83,11 +83,13 @@ public partial class Gambling
 | 
			
		||||
                        toInsert = 0;
 | 
			
		||||
                    else if (randomNumber != 1)
 | 
			
		||||
                        for (var j = 0; j < dice.Count; j++)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (values[j] < randomNumber)
 | 
			
		||||
                            {
 | 
			
		||||
                                toInsert = j;
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
@@ -100,7 +102,8 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
            using var bitmap = dice.Merge(out var format);
 | 
			
		||||
            await using var ms = bitmap.ToStream(format);
 | 
			
		||||
            foreach (var d in dice) d.Dispose();
 | 
			
		||||
            foreach (var d in dice)
 | 
			
		||||
                d.Dispose();
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendFileAsync(ms,
 | 
			
		||||
                $"dice.{format.FileExtensions.First()}",
 | 
			
		||||
@@ -123,7 +126,8 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
                var rolls = new List<char>();
 | 
			
		||||
 | 
			
		||||
                for (var i = 0; i < n1; i++) rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
 | 
			
		||||
                for (var i = 0; i < n1; i++)
 | 
			
		||||
                    rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithDescription(ctx.User.Mention
 | 
			
		||||
@@ -150,7 +154,8 @@ public partial class Gambling
 | 
			
		||||
                        sub = 0;
 | 
			
		||||
 | 
			
		||||
                    var arr = new int[n1];
 | 
			
		||||
                    for (var i = 0; i < n1; i++) arr[i] = rng.Next(1, n2 + 1);
 | 
			
		||||
                    for (var i = 0; i < n1; i++)
 | 
			
		||||
                        arr[i] = rng.Next(1, n2 + 1);
 | 
			
		||||
 | 
			
		||||
                    var sum = arr.Sum();
 | 
			
		||||
                    var embed = _eb.Create()
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,8 @@ public partial class Gambling
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var img = images.Merge();
 | 
			
		||||
            foreach (var i in images) i.Dispose();
 | 
			
		||||
            foreach (var i in images)
 | 
			
		||||
                i.Dispose();
 | 
			
		||||
 | 
			
		||||
            var toSend = $"{Format.Bold(ctx.User.ToString())}";
 | 
			
		||||
            if (cardObjects.Count == 5)
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,8 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
    {
 | 
			
		||||
        var potEmpty = PotEmptied;
 | 
			
		||||
        var toAward = new List<ulong>();
 | 
			
		||||
        while (_toAward.TryDequeue(out var x)) toAward.Add(x);
 | 
			
		||||
        while (_toAward.TryDequeue(out var x))
 | 
			
		||||
            toAward.Add(x);
 | 
			
		||||
 | 
			
		||||
        if (!toAward.Any())
 | 
			
		||||
            return;
 | 
			
		||||
@@ -85,7 +86,10 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
                    {
 | 
			
		||||
                        m.Embed = GetEmbed(PotSize).Build();
 | 
			
		||||
                    },
 | 
			
		||||
                    new() { RetryMode = RetryMode.AlwaysRetry });
 | 
			
		||||
                    new()
 | 
			
		||||
                    {
 | 
			
		||||
                        RetryMode = RetryMode.AlwaysRetry
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
            Log.Information("Awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
                toAward.Count,
 | 
			
		||||
@@ -93,9 +97,7 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
                _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 | 
			
		||||
            if (potEmpty)
 | 
			
		||||
            {
 | 
			
		||||
                _= StopEvent();
 | 
			
		||||
            }
 | 
			
		||||
                _ = StopEvent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -117,7 +119,8 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
 | 
			
		||||
    private async Task OnMessageDeleted(Cacheable<IMessage, ulong> message, Cacheable<IMessageChannel, ulong> cacheable)
 | 
			
		||||
    {
 | 
			
		||||
        if (message.Id == msg.Id) await StopEvent();
 | 
			
		||||
        if (message.Id == msg.Id)
 | 
			
		||||
            await StopEvent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task StopEvent()
 | 
			
		||||
@@ -135,7 +138,7 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
            _ = _client.SetGameAsync(null);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _= msg.DeleteAsync();
 | 
			
		||||
                _ = msg.DeleteAsync();
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
 | 
			
		||||
@@ -145,7 +148,7 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
 | 
			
		||||
    private Task HandleMessage(SocketMessage message)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            if (message.Author is not IGuildUser gu // no unknown users, as they could be bots, or alts
 | 
			
		||||
                || gu.IsBot // no bots
 | 
			
		||||
@@ -163,7 +166,10 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await message.DeleteAsync(new() { RetryMode = RetryMode.AlwaysFail });
 | 
			
		||||
                await message.DeleteAsync(new()
 | 
			
		||||
                {
 | 
			
		||||
                    RetryMode = RetryMode.AlwaysFail
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -58,13 +58,14 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void EventTimeout(object state)
 | 
			
		||||
        => _= StopEvent();
 | 
			
		||||
        => _ = StopEvent();
 | 
			
		||||
 | 
			
		||||
    private async void OnTimerTick(object state)
 | 
			
		||||
    {
 | 
			
		||||
        var potEmpty = PotEmptied;
 | 
			
		||||
        var toAward = new List<ulong>();
 | 
			
		||||
        while (_toAward.TryDequeue(out var x)) toAward.Add(x);
 | 
			
		||||
        while (_toAward.TryDequeue(out var x))
 | 
			
		||||
            toAward.Add(x);
 | 
			
		||||
 | 
			
		||||
        if (!toAward.Any())
 | 
			
		||||
            return;
 | 
			
		||||
@@ -78,7 +79,10 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
                    {
 | 
			
		||||
                        m.Embed = GetEmbed(PotSize).Build();
 | 
			
		||||
                    },
 | 
			
		||||
                    new() { RetryMode = RetryMode.AlwaysRetry });
 | 
			
		||||
                    new()
 | 
			
		||||
                    {
 | 
			
		||||
                        RetryMode = RetryMode.AlwaysRetry
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
            Log.Information("Awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
                toAward.Count,
 | 
			
		||||
@@ -86,9 +90,7 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
                _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 | 
			
		||||
            if (potEmpty)
 | 
			
		||||
            {
 | 
			
		||||
                _= StopEvent();
 | 
			
		||||
            }
 | 
			
		||||
                _ = StopEvent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -114,7 +116,8 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
 | 
			
		||||
    private async Task OnMessageDeleted(Cacheable<IMessage, ulong> message, Cacheable<IMessageChannel, ulong> cacheable)
 | 
			
		||||
    {
 | 
			
		||||
        if (message.Id == msg.Id) await StopEvent();
 | 
			
		||||
        if (message.Id == msg.Id)
 | 
			
		||||
            await StopEvent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task StopEvent()
 | 
			
		||||
@@ -131,7 +134,7 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
            _timeout?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _= msg.DeleteAsync();
 | 
			
		||||
                _ = msg.DeleteAsync();
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +147,7 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> cacheable,
 | 
			
		||||
        SocketReaction r)
 | 
			
		||||
    {
 | 
			
		||||
        _= Task.Run(() =>
 | 
			
		||||
        _ = Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            if (emote.Name != r.Emote.Name)
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,8 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
            using var img = imgs.Merge(out var format);
 | 
			
		||||
            await using var stream = img.ToStream(format);
 | 
			
		||||
            foreach (var i in imgs) i.Dispose();
 | 
			
		||||
            foreach (var i in imgs)
 | 
			
		||||
                i.Dispose();
 | 
			
		||||
            var msg = count != 1
 | 
			
		||||
                ? Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_results(count, headCount, tailCount))
 | 
			
		||||
                : Format.Bold(ctx.User.ToString())
 | 
			
		||||
 
 | 
			
		||||
@@ -86,11 +86,11 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    {
 | 
			
		||||
        var ec = _service.GetEconomy();
 | 
			
		||||
        decimal onePercent = 0;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // This stops the top 1% from owning more than 100% of the money
 | 
			
		||||
        if (ec.Cash > 0)
 | 
			
		||||
            onePercent = ec.OnePercent / (ec.Cash - ec.Bot);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // [21:03] Bob Page: Kinda remids me of US economy
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithTitle(GetText(strs.economy_state))
 | 
			
		||||
@@ -103,7 +103,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
                       .AddField(GetText(strs.total),
 | 
			
		||||
                           ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", Culture) + CurrencySign)
 | 
			
		||||
                       .WithOkColor();
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
@@ -165,7 +165,8 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
        var members = (await role.GetMembersAsync()).Where(u => u.Status != UserStatus.Offline);
 | 
			
		||||
        var membersArray = members as IUser[] ?? members.ToArray();
 | 
			
		||||
        if (membersArray.Length == 0) return;
 | 
			
		||||
        if (membersArray.Length == 0)
 | 
			
		||||
            return;
 | 
			
		||||
        var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
 | 
			
		||||
        await SendConfirmAsync("🎟 " + GetText(strs.raffled_user),
 | 
			
		||||
            $"**{usr.Username}#{usr.Discriminator}**",
 | 
			
		||||
@@ -180,7 +181,8 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
        var members = await role.GetMembersAsync();
 | 
			
		||||
        var membersArray = members as IUser[] ?? members.ToArray();
 | 
			
		||||
        if (membersArray.Length == 0) return;
 | 
			
		||||
        if (membersArray.Length == 0)
 | 
			
		||||
            return;
 | 
			
		||||
        var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
 | 
			
		||||
        await SendConfirmAsync("🎟 " + GetText(strs.raffled_user),
 | 
			
		||||
            $"**{usr.Username}#{usr.Discriminator}**",
 | 
			
		||||
@@ -228,13 +230,13 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
            var change = tr.Amount >= 0 ? "🔵" : "🔴";
 | 
			
		||||
            var kwumId = new kwum(tr.Id).ToString();
 | 
			
		||||
            var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
 | 
			
		||||
            
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
 | 
			
		||||
            var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
 | 
			
		||||
            if(transactionString is not null)
 | 
			
		||||
            if (transactionString is not null)
 | 
			
		||||
                sb.AppendLine(transactionString);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(tr.Note))
 | 
			
		||||
                sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
 | 
			
		||||
        }
 | 
			
		||||
@@ -265,7 +267,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var eb = _eb.Create(ctx)
 | 
			
		||||
           .WithOkColor();
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        eb.WithAuthor(ctx.User);
 | 
			
		||||
        eb.WithTitle(GetText(strs.transaction));
 | 
			
		||||
@@ -273,16 +275,16 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        eb.AddField("Amount", N(tr.Amount));
 | 
			
		||||
        eb.AddField("Type", tr.Type, true);
 | 
			
		||||
        eb.AddField("Extra", tr.Extra, true);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (tr.OtherId is ulong other)
 | 
			
		||||
            eb.AddField("From Id", other);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(tr.Note))
 | 
			
		||||
            eb.AddField("Note", tr.Note);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        eb.WithFooter(GetFormattedCurtrDate(tr));
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -325,7 +327,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    {
 | 
			
		||||
        if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (!await _cs.TransferAsync(ctx.User.Id, receiver.Id, amount, ctx.User.ToString(), msg))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
@@ -373,7 +375,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
        await _cs.AddAsync(usr.Id,
 | 
			
		||||
            amount,
 | 
			
		||||
            new TxData("award", ctx.User.ToString()!, msg, ctx.User.Id)
 | 
			
		||||
            new("award", ctx.User.ToString()!, msg, ctx.User.Id)
 | 
			
		||||
        );
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.awarded(N(amount), $"<@{usrId}>"));
 | 
			
		||||
    }
 | 
			
		||||
@@ -431,7 +433,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
            ctx.User.ToString()!,
 | 
			
		||||
            null,
 | 
			
		||||
            ctx.User.Id);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (await _cs.RemoveAsync(user.Id, amount, extra))
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.take(N(amount), Format.Bold(user.ToString())));
 | 
			
		||||
        else
 | 
			
		||||
@@ -450,7 +452,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
            ctx.User.ToString()!,
 | 
			
		||||
            null,
 | 
			
		||||
            ctx.User.Id);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (await _cs.RemoveAsync(usrId, amount, extra))
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.take(N(amount), $"<@{usrId}>"));
 | 
			
		||||
        else
 | 
			
		||||
@@ -689,7 +691,6 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3);
 | 
			
		||||
 | 
			
		||||
        if (amount > 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await _cs.RemoveAsync(ctx.User.Id,
 | 
			
		||||
                    amount,
 | 
			
		||||
                    new("rps", "bet", "")))
 | 
			
		||||
@@ -697,7 +698,6 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        string msg;
 | 
			
		||||
        if (pick == nadekoPick)
 | 
			
		||||
 
 | 
			
		||||
@@ -109,8 +109,21 @@ Doesn't have to be ordered.")]
 | 
			
		||||
    public BetRollConfig()
 | 
			
		||||
        => Pairs = new BetRollPair[]
 | 
			
		||||
        {
 | 
			
		||||
            new() { WhenAbove = 99, MultiplyBy = 10 }, new() { WhenAbove = 90, MultiplyBy = 4 },
 | 
			
		||||
            new() { WhenAbove = 66, MultiplyBy = 2 }
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                WhenAbove = 99,
 | 
			
		||||
                MultiplyBy = 10
 | 
			
		||||
            },
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                WhenAbove = 90,
 | 
			
		||||
                MultiplyBy = 4
 | 
			
		||||
            },
 | 
			
		||||
            new()
 | 
			
		||||
            {
 | 
			
		||||
                WhenAbove = 66,
 | 
			
		||||
                MultiplyBy = 2
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
 | 
			
		||||
{
 | 
			
		||||
    private const string FILE_PATH = "data/gambling.yml";
 | 
			
		||||
    private static readonly TypedKey<GamblingConfig> _changeKey = new("config.gambling.updated");
 | 
			
		||||
 | 
			
		||||
    public override string Name
 | 
			
		||||
        => "gambling";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,10 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
 | 
			
		||||
        var takeRes = await _cs.RemoveAsync(userId, amount, new("slot", "bet"));
 | 
			
		||||
 | 
			
		||||
        if (!takeRes)
 | 
			
		||||
            return new() { Error = GamblingError.NotEnough };
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = GamblingError.NotEnough
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        var game = new SlotGame();
 | 
			
		||||
        var result = game.Spin();
 | 
			
		||||
@@ -99,7 +102,11 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
 | 
			
		||||
            await _cs.AddAsync(userId, won, new("slot", "win", $"Slot Machine x{result.Multiplier}"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var toReturn = new SlotResponse { Multiplier = result.Multiplier, Won = won };
 | 
			
		||||
        var toReturn = new SlotResponse
 | 
			
		||||
        {
 | 
			
		||||
            Multiplier = result.Multiplier,
 | 
			
		||||
            Won = won
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        toReturn.Rolls.AddRange(result.Rolls);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,8 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> InternalCheckBet(long amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (amount < 1) return false;
 | 
			
		||||
        if (amount < 1)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (amount < Config.MinBet)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.min_bet_limit(Format.Bold(Config.MinBet.ToString()) + CurrencySign));
 | 
			
		||||
@@ -39,13 +40,15 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
 | 
			
		||||
 | 
			
		||||
    protected Task<bool> CheckBetMandatory(long amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (amount < 1) return Task.FromResult(false);
 | 
			
		||||
        if (amount < 1)
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
        return InternalCheckBet(amount);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected Task<bool> CheckBetOptional(long amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (amount == 0) return Task.FromResult(true);
 | 
			
		||||
        if (amount == 0)
 | 
			
		||||
            return Task.FromResult(true);
 | 
			
		||||
        return InternalCheckBet(amount);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,14 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
        public PlantPickCommands(ILogCommandService logService, GamblingConfigService gss)
 | 
			
		||||
            : base(gss)
 | 
			
		||||
            => this._logService = logService;
 | 
			
		||||
            => _logService = logService;
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async partial Task Pick(string pass = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) return;
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var picked = await _service.PickAsync(ctx.Guild.Id, (ITextChannel)ctx.Channel, ctx.User.Id, pass);
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +46,8 @@ public partial class Gambling
 | 
			
		||||
            if (amount < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) return;
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
 | 
			
		||||
            {
 | 
			
		||||
@@ -59,7 +61,8 @@ public partial class Gambling
 | 
			
		||||
                ctx.User.ToString(),
 | 
			
		||||
                amount,
 | 
			
		||||
                pass);
 | 
			
		||||
            if (!success) await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
            if (!success)
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,10 @@ public class PlantPickService : INService
 | 
			
		||||
        using var uow = _db.GetDbContext();
 | 
			
		||||
        var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
 | 
			
		||||
 | 
			
		||||
        var toAdd = new GCChannelId { ChannelId = cid };
 | 
			
		||||
        var toAdd = new GCChannelId
 | 
			
		||||
        {
 | 
			
		||||
            ChannelId = cid
 | 
			
		||||
        };
 | 
			
		||||
        if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
 | 
			
		||||
        {
 | 
			
		||||
            guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
 | 
			
		||||
@@ -81,7 +84,8 @@ public class PlantPickService : INService
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
 | 
			
		||||
            if (toDelete is not null) uow.Remove(toDelete);
 | 
			
		||||
            if (toDelete is not null)
 | 
			
		||||
                uow.Remove(toDelete);
 | 
			
		||||
            _generationChannels.TryRemove(cid);
 | 
			
		||||
            enabled = false;
 | 
			
		||||
        }
 | 
			
		||||
@@ -172,7 +176,7 @@ public class PlantPickService : INService
 | 
			
		||||
        if (!_generationChannels.Contains(channel.Id))
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        _= Task.Run(async () =>
 | 
			
		||||
        _ = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -277,7 +281,7 @@ public class PlantPickService : INService
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // delete all of the plant messages which have just been picked
 | 
			
		||||
                _= ch.DeleteMessagesAsync(ids);
 | 
			
		||||
                _ = ch.DeleteMessagesAsync(ids);
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,11 @@ public class CurrencyRaffleGame
 | 
			
		||||
        if (GameType == Type.Normal && _users.Count > 0 && _users.First().Amount != amount)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (!_users.Add(new() { DiscordUser = usr, Amount = amount }))
 | 
			
		||||
        if (!_users.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                DiscordUser = usr,
 | 
			
		||||
                Amount = amount
 | 
			
		||||
            }))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,6 @@ public class CurrencyRaffleService : INService
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (newGame)
 | 
			
		||||
            {
 | 
			
		||||
                _ = Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(60000);
 | 
			
		||||
@@ -69,7 +68,6 @@ public class CurrencyRaffleService : INService
 | 
			
		||||
                    catch { }
 | 
			
		||||
                    finally { _locker.Release(); }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (crg, null);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -225,7 +225,10 @@ public partial class Gambling
 | 
			
		||||
                var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigsForId(ctx.Guild.Id,
 | 
			
		||||
                                                                      set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                                                          .ThenInclude(x => x.Items))
 | 
			
		||||
                                                                  .ShopEntries) { entry };
 | 
			
		||||
                                                                  .ShopEntries)
 | 
			
		||||
                {
 | 
			
		||||
                    entry
 | 
			
		||||
                };
 | 
			
		||||
                uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries;
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
@@ -254,7 +257,10 @@ public partial class Gambling
 | 
			
		||||
                var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigsForId(ctx.Guild.Id,
 | 
			
		||||
                                                                      set => set.Include(x => x.ShopEntries)
 | 
			
		||||
                                                                          .ThenInclude(x => x.Items))
 | 
			
		||||
                                                                  .ShopEntries) { entry };
 | 
			
		||||
                                                                  .ShopEntries)
 | 
			
		||||
                {
 | 
			
		||||
                    entry
 | 
			
		||||
                };
 | 
			
		||||
                uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries;
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
@@ -270,7 +276,10 @@ public partial class Gambling
 | 
			
		||||
            index -= 1;
 | 
			
		||||
            if (index < 0)
 | 
			
		||||
                return;
 | 
			
		||||
            var item = new ShopEntryItem { Text = itemText };
 | 
			
		||||
            var item = new ShopEntryItem
 | 
			
		||||
            {
 | 
			
		||||
                Text = itemText
 | 
			
		||||
            };
 | 
			
		||||
            ShopEntry entry;
 | 
			
		||||
            var rightType = false;
 | 
			
		||||
            var added = false;
 | 
			
		||||
@@ -414,7 +423,6 @@ public partial class Gambling
 | 
			
		||||
            var embed = _eb.Create().WithOkColor();
 | 
			
		||||
 | 
			
		||||
            if (entry.Type == ShopEntryType.Role)
 | 
			
		||||
            {
 | 
			
		||||
                return embed
 | 
			
		||||
                       .AddField(GetText(strs.name),
 | 
			
		||||
                           GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name
 | 
			
		||||
@@ -422,14 +430,11 @@ public partial class Gambling
 | 
			
		||||
                           true)
 | 
			
		||||
                       .AddField(GetText(strs.price), entry.Price.ToString(), true)
 | 
			
		||||
                       .AddField(GetText(strs.type), entry.Type.ToString(), true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (entry.Type == ShopEntryType.List)
 | 
			
		||||
            {
 | 
			
		||||
                return embed.AddField(GetText(strs.name), entry.Name, true)
 | 
			
		||||
                            .AddField(GetText(strs.price), entry.Price.ToString(), true)
 | 
			
		||||
                            .AddField(GetText(strs.type), GetText(strs.random_unique_item), true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //else if (entry.Type == ShopEntryType.Infinite_List)
 | 
			
		||||
            //    return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(entry.RoleName)), true))
 | 
			
		||||
 
 | 
			
		||||
@@ -211,7 +211,7 @@ public partial class Gambling
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                _= Task.Run(async () =>
 | 
			
		||||
                _ = Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(1000);
 | 
			
		||||
                    _runningUsers.Remove(ctx.User.Id);
 | 
			
		||||
@@ -238,7 +238,8 @@ public partial class Gambling
 | 
			
		||||
            public static SlotResult Pull()
 | 
			
		||||
            {
 | 
			
		||||
                var numbers = new int[3];
 | 
			
		||||
                for (var i = 0; i < numbers.Length; i++) numbers[i] = new NadekoRandom().Next(0, MAX_VALUE + 1);
 | 
			
		||||
                for (var i = 0; i < numbers.Length; i++)
 | 
			
		||||
                    numbers[i] = new NadekoRandom().Next(0, MAX_VALUE + 1);
 | 
			
		||||
                var multi = 0;
 | 
			
		||||
                foreach (var t in _winningCombos)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,8 @@ public class VoteRewardService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
        var http = new HttpClient(new HttpClientHandler
 | 
			
		||||
        {
 | 
			
		||||
            AllowAutoRedirect = false, ServerCertificateCustomValidationCallback = delegate { return true; }
 | 
			
		||||
            AllowAutoRedirect = false,
 | 
			
		||||
            ServerCertificateCustomValidationCallback = delegate { return true; }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        while (true)
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,8 @@ public partial class Gambling
 | 
			
		||||
        public partial Task Divorce([Leftover] string target)
 | 
			
		||||
        {
 | 
			
		||||
            var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target);
 | 
			
		||||
            if (waifuUserId == default) return ReplyErrorLocalizedAsync(strs.waifu_not_yours);
 | 
			
		||||
            if (waifuUserId == default)
 | 
			
		||||
                return ReplyErrorLocalizedAsync(strs.waifu_not_yours);
 | 
			
		||||
 | 
			
		||||
            return Divorce(waifuUserId);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,8 @@ public class WaifuService : INService
 | 
			
		||||
        }
 | 
			
		||||
        else // if not, pay 10% fee
 | 
			
		||||
        {
 | 
			
		||||
            if (!await _cs.RemoveAsync(owner.Id, waifu.Price / 10, new("waifu", "transfer"))) return false;
 | 
			
		||||
            if (!await _cs.RemoveAsync(owner.Id, waifu.Price / 10, new("waifu", "transfer")))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            waifu.Price = (int)(waifu.Price * 0.95); // half of 10% = 5% price reduction
 | 
			
		||||
            if (waifu.Price < settings.Waifu.MinPrice)
 | 
			
		||||
@@ -150,10 +151,19 @@ public class WaifuService : INService
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    uow.WaifuInfo.Add(w = new() { Waifu = waifu, Claimer = claimer, Affinity = null, Price = amount });
 | 
			
		||||
                    uow.WaifuInfo.Add(w = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Waifu = waifu,
 | 
			
		||||
                        Claimer = claimer,
 | 
			
		||||
                        Affinity = null,
 | 
			
		||||
                        Price = amount
 | 
			
		||||
                    });
 | 
			
		||||
                    uow.WaifuUpdates.Add(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = waifu, Old = null, New = claimer, UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        User = waifu,
 | 
			
		||||
                        Old = null,
 | 
			
		||||
                        New = claimer,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                    });
 | 
			
		||||
                    result = WaifuClaimResult.Success;
 | 
			
		||||
                }
 | 
			
		||||
@@ -173,7 +183,10 @@ public class WaifuService : INService
 | 
			
		||||
 | 
			
		||||
                    uow.WaifuUpdates.Add(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = w.Waifu, Old = oldClaimer, New = w.Claimer, UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        User = w.Waifu,
 | 
			
		||||
                        Old = oldClaimer,
 | 
			
		||||
                        New = w.Claimer,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -192,7 +205,10 @@ public class WaifuService : INService
 | 
			
		||||
 | 
			
		||||
                    uow.WaifuUpdates.Add(new()
 | 
			
		||||
                    {
 | 
			
		||||
                        User = w.Waifu, Old = oldClaimer, New = w.Claimer, UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                        User = w.Waifu,
 | 
			
		||||
                        Old = oldClaimer,
 | 
			
		||||
                        New = w.Claimer,
 | 
			
		||||
                        UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -226,12 +242,21 @@ public class WaifuService : INService
 | 
			
		||||
            else if (w is null)
 | 
			
		||||
            {
 | 
			
		||||
                var thisUser = uow.GetOrCreateUser(user);
 | 
			
		||||
                uow.WaifuInfo.Add(new() { Affinity = newAff, Waifu = thisUser, Price = 1, Claimer = null });
 | 
			
		||||
                uow.WaifuInfo.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    Affinity = newAff,
 | 
			
		||||
                    Waifu = thisUser,
 | 
			
		||||
                    Price = 1,
 | 
			
		||||
                    Claimer = null
 | 
			
		||||
                });
 | 
			
		||||
                success = true;
 | 
			
		||||
 | 
			
		||||
                uow.WaifuUpdates.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    User = thisUser, Old = null, New = newAff, UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                    User = thisUser,
 | 
			
		||||
                    Old = null,
 | 
			
		||||
                    New = newAff,
 | 
			
		||||
                    UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
@@ -243,7 +268,10 @@ public class WaifuService : INService
 | 
			
		||||
 | 
			
		||||
                uow.WaifuUpdates.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    User = w.Waifu, Old = oldAff, New = newAff, UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                    User = w.Waifu,
 | 
			
		||||
                    Old = oldAff,
 | 
			
		||||
                    New = newAff,
 | 
			
		||||
                    UpdateType = WaifuUpdateType.AffinityChanged
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +332,10 @@ public class WaifuService : INService
 | 
			
		||||
 | 
			
		||||
                uow.WaifuUpdates.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    User = w.Waifu, Old = oldClaimer, New = null, UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                    User = w.Waifu,
 | 
			
		||||
                    Old = oldClaimer,
 | 
			
		||||
                    New = null,
 | 
			
		||||
                    UpdateType = WaifuUpdateType.Claimed
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -316,19 +347,27 @@ public class WaifuService : INService
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item"))) return false;
 | 
			
		||||
        if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item")))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var w = uow.WaifuInfo.ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer));
 | 
			
		||||
        if (w is null)
 | 
			
		||||
            uow.WaifuInfo.Add(w = new()
 | 
			
		||||
            {
 | 
			
		||||
                Affinity = null, Claimer = null, Price = 1, Waifu = uow.GetOrCreateUser(giftedWaifu)
 | 
			
		||||
                Affinity = null,
 | 
			
		||||
                Claimer = null,
 | 
			
		||||
                Price = 1,
 | 
			
		||||
                Waifu = uow.GetOrCreateUser(giftedWaifu)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (!itemObj.Negative)
 | 
			
		||||
        {
 | 
			
		||||
            w.Items.Add(new() { Name = itemObj.Name.ToLowerInvariant(), ItemEmoji = itemObj.ItemEmoji });
 | 
			
		||||
            w.Items.Add(new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = itemObj.Name.ToLowerInvariant(),
 | 
			
		||||
                ItemEmoji = itemObj.ItemEmoji
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (w.Claimer?.UserId == from.Id)
 | 
			
		||||
                w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,11 @@ public class WheelOfFortuneGame
 | 
			
		||||
        if (amount > 0)
 | 
			
		||||
            await _cs.AddAsync(_userId, amount, new("wheel", "win"));
 | 
			
		||||
 | 
			
		||||
        return new() { Index = result, Amount = amount };
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Index = result,
 | 
			
		||||
            Amount = amount
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Result
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,18 @@ public class Betroll
 | 
			
		||||
 | 
			
		||||
        var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll);
 | 
			
		||||
        if (pair is null)
 | 
			
		||||
            return new() { Multiplier = 0, Roll = roll };
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Multiplier = 0,
 | 
			
		||||
                Roll = roll
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        return new() { Multiplier = pair.MultiplyBy, Roll = roll, Threshold = pair.WhenAbove };
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Multiplier = pair.MultiplyBy,
 | 
			
		||||
            Roll = roll,
 | 
			
		||||
            Threshold = pair.WhenAbove
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Result
 | 
			
		||||
 
 | 
			
		||||
@@ -98,7 +98,8 @@ public class Deck
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private void Shuffle()
 | 
			
		||||
    {
 | 
			
		||||
        if (CardPool.Count <= 1) return;
 | 
			
		||||
        if (CardPool.Count <= 1)
 | 
			
		||||
            return;
 | 
			
		||||
        var orderedPool = CardPool.Shuffle();
 | 
			
		||||
        CardPool ??= orderedPool.ToList();
 | 
			
		||||
    }
 | 
			
		||||
@@ -128,7 +129,8 @@ public class Deck
 | 
			
		||||
            if (cards.GroupBy(card => card.Number).Count() != cards.Count())
 | 
			
		||||
                return false;
 | 
			
		||||
            var toReturn = cards.Max(card => card.Number) - cards.Min(card => card.Number) == 4;
 | 
			
		||||
            if (toReturn || cards.All(c => c.Number != 1)) return toReturn;
 | 
			
		||||
            if (toReturn || cards.All(c => c.Number != 1))
 | 
			
		||||
                return toReturn;
 | 
			
		||||
 | 
			
		||||
            var newCards = cards.Select(c => c.Number == 1 ? new(c.Suit, 14) : c).ToArray();
 | 
			
		||||
            return newCards.Max(card => card.Number) - newCards.Min(card => card.Number) == 4;
 | 
			
		||||
@@ -194,8 +196,9 @@ public class Deck
 | 
			
		||||
    {
 | 
			
		||||
        if (handValues is null)
 | 
			
		||||
            InitHandValues();
 | 
			
		||||
        
 | 
			
		||||
        foreach (var kvp in handValues.Where(x => x.Value(cards))) return kvp.Key;
 | 
			
		||||
 | 
			
		||||
        foreach (var kvp in handValues.Where(x => x.Value(cards)))
 | 
			
		||||
            return kvp.Key;
 | 
			
		||||
        return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -203,7 +206,10 @@ public class Deck
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly IReadOnlyDictionary<CardSuit, string> _suitToSuitChar = new Dictionary<CardSuit, string>
 | 
			
		||||
        {
 | 
			
		||||
            { CardSuit.Diamonds, "♦" }, { CardSuit.Clubs, "♣" }, { CardSuit.Spades, "♠" }, { CardSuit.Hearts, "♥" }
 | 
			
		||||
            { CardSuit.Diamonds, "♦" },
 | 
			
		||||
            { CardSuit.Clubs, "♣" },
 | 
			
		||||
            { CardSuit.Spades, "♠" },
 | 
			
		||||
            { CardSuit.Hearts, "♥" }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        private static readonly IReadOnlyDictionary<string, CardSuit> _suitCharToSuit = new Dictionary<string, CardSuit>
 | 
			
		||||
@@ -272,7 +278,8 @@ public class Deck
 | 
			
		||||
 | 
			
		||||
        public int CompareTo(object obj)
 | 
			
		||||
        {
 | 
			
		||||
            if (obj is not Card card) return 0;
 | 
			
		||||
            if (obj is not Card card)
 | 
			
		||||
                return 0;
 | 
			
		||||
            return Number - card.Number;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,7 @@ public sealed class AcrophobiaGame : IDisposable
 | 
			
		||||
                        || !_usersWhoVoted.Add(userId))
 | 
			
		||||
                        break;
 | 
			
		||||
                    ++_submissions[toVoteFor];
 | 
			
		||||
                    _= Task.Run(() => OnUserVoted(userName));
 | 
			
		||||
                    _ = Task.Run(() => OnUserVoted(userName));
 | 
			
		||||
                    return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ public partial class Games
 | 
			
		||||
                if (msg.Channel.Id != ctx.Channel.Id)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                _= Task.Run(async () =>
 | 
			
		||||
                _ = Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
 
 | 
			
		||||
@@ -137,7 +137,7 @@ Message: {Content}",
 | 
			
		||||
                    usrMsg.Author,
 | 
			
		||||
                    usrMsg.Author.Id,
 | 
			
		||||
                    usrMsg.Content);
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,17 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
 | 
			
		||||
    public int Version { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment("Hangman related settings (.hangman command)")]
 | 
			
		||||
    public HangmanConfig Hangman { get; set; } = new() { CurrencyReward = 0 };
 | 
			
		||||
    public HangmanConfig Hangman { get; set; } = new()
 | 
			
		||||
    {
 | 
			
		||||
        CurrencyReward = 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    [Comment("Trivia related settings (.t command)")]
 | 
			
		||||
    public TriviaConfig Trivia { get; set; } = new() { CurrencyReward = 0, MinimumWinReq = 1 };
 | 
			
		||||
    public TriviaConfig Trivia { get; set; } = new()
 | 
			
		||||
    {
 | 
			
		||||
        CurrencyReward = 0,
 | 
			
		||||
        MinimumWinReq = 1
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    [Comment("List of responses for the .8ball command. A random one will be selected every time")]
 | 
			
		||||
    public List<string> EightBallResponses { get; set; } = new()
 | 
			
		||||
@@ -47,14 +54,46 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
 | 
			
		||||
    [Comment("List of animals which will be used for the animal race game (.race)")]
 | 
			
		||||
    public List<RaceAnimal> RaceAnimals { get; set; } = new()
 | 
			
		||||
    {
 | 
			
		||||
        new() { Icon = "🐼", Name = "Panda" },
 | 
			
		||||
        new() { Icon = "🐻", Name = "Bear" },
 | 
			
		||||
        new() { Icon = "🐧", Name = "Pengu" },
 | 
			
		||||
        new() { Icon = "🐨", Name = "Koala" },
 | 
			
		||||
        new() { Icon = "🐬", Name = "Dolphin" },
 | 
			
		||||
        new() { Icon = "🐞", Name = "Ladybird" },
 | 
			
		||||
        new() { Icon = "🦀", Name = "Crab" },
 | 
			
		||||
        new() { Icon = "🦄", Name = "Unicorn" }
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐼",
 | 
			
		||||
            Name = "Panda"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐻",
 | 
			
		||||
            Name = "Bear"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐧",
 | 
			
		||||
            Name = "Pengu"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐨",
 | 
			
		||||
            Name = "Koala"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐬",
 | 
			
		||||
            Name = "Dolphin"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🐞",
 | 
			
		||||
            Name = "Ladybird"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🦀",
 | 
			
		||||
            Name = "Crab"
 | 
			
		||||
        },
 | 
			
		||||
        new()
 | 
			
		||||
        {
 | 
			
		||||
            Icon = "🦄",
 | 
			
		||||
            Name = "Unicorn"
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,10 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
 | 
			
		||||
            ModifyConfig(c =>
 | 
			
		||||
            {
 | 
			
		||||
                c.Version = 1;
 | 
			
		||||
                c.Hangman = new() { CurrencyReward = 0 };
 | 
			
		||||
                c.Hangman = new()
 | 
			
		||||
                {
 | 
			
		||||
                    CurrencyReward = 0
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -38,7 +38,10 @@ public class GamesService : INService
 | 
			
		||||
    {
 | 
			
		||||
        _gamesConfig = gamesConfig;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
        _8BallCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 500_000 });
 | 
			
		||||
        _8BallCache = new MemoryCache(new MemoryCacheOptions
 | 
			
		||||
        {
 | 
			
		||||
            SizeLimit = 500_000
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Ratings = new(GetRatingTexts);
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
 | 
			
		||||
 | 
			
		||||
        var qs = new Dictionary<string, HangmanTerm[]>();
 | 
			
		||||
        foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml"))
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = Yaml.Deserializer.Deserialize<HangmanTerm[]>(File.ReadAllText(file));
 | 
			
		||||
@@ -33,6 +34,7 @@ public sealed class DefaultHangmanSource : IHangmanSource
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Loading {HangmanFile} failed", file);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        termsDict = qs;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,8 @@ public partial class Games
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async partial Task HangmanStop()
 | 
			
		||||
        {
 | 
			
		||||
            if (await _service.StopHangman(ctx.Channel.Id)) await ReplyConfirmLocalizedAsync(strs.hangman_stopped);
 | 
			
		||||
            if (await _service.StopHangman(ctx.Channel.Id))
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.hangman_stopped);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -54,7 +54,8 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
 | 
			
		||||
    {
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (_hangmanGames.TryRemove(channelId, out _)) return new(true);
 | 
			
		||||
            if (_hangmanGames.TryRemove(channelId, out _))
 | 
			
		||||
                return new(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new(false);
 | 
			
		||||
@@ -88,7 +89,10 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
 | 
			
		||||
                if (state.GuessResult is HangmanGame.GuessResult.Incorrect or HangmanGame.GuessResult.AlreadyTried)
 | 
			
		||||
                    _cdCache.Set(msg.Author.Id,
 | 
			
		||||
                        string.Empty,
 | 
			
		||||
                        new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3) });
 | 
			
		||||
                        new MemoryCacheEntryOptions
 | 
			
		||||
                        {
 | 
			
		||||
                            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                if (state.Phase == HangmanGame.Phase.Ended)
 | 
			
		||||
                    if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ public sealed class NunchiGame : IDisposable
 | 
			
		||||
                KILL_TIMEOUT);
 | 
			
		||||
 | 
			
		||||
            CurrentPhase = Phase.Playing;
 | 
			
		||||
            _= OnGameStarted?.Invoke(this);
 | 
			
		||||
            _ = OnGameStarted?.Invoke(this);
 | 
			
		||||
            _ = OnRoundStarted?.Invoke(this, CurrentNumber);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
@@ -122,7 +122,7 @@ public sealed class NunchiGame : IDisposable
 | 
			
		||||
                    {
 | 
			
		||||
                        killTimer.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                        CurrentPhase = Phase.Ended;
 | 
			
		||||
                        _= OnGameEnded?.Invoke(this, userTuple.Name);
 | 
			
		||||
                        _ = OnGameEnded?.Invoke(this, userTuple.Name);
 | 
			
		||||
                    }
 | 
			
		||||
                    else // else just start the new round without the user who was the last
 | 
			
		||||
                    {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user