mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Added and applied styles for private readonly fields, private fields to Extensions and Common folders.
- Some renamings and code cleanups - Chained method calls, binary expressions and binary patterns will now break into newlines - Type param constraints and base constructor calls will be on the new line
This commit is contained in:
		@@ -173,6 +173,14 @@ csharp_preserve_single_line_statements = true
 | 
			
		||||
 | 
			
		||||
# Naming rules
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
 | 
			
		||||
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
 | 
			
		||||
dotnet_naming_rule.private_readonly_field.severity = warning
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.private_field.symbols = private_field
 | 
			
		||||
dotnet_naming_rule.private_field.style = camel_case
 | 
			
		||||
dotnet_naming_rule.private_field.severity = warning
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.const_fields.symbols = const_fields
 | 
			
		||||
dotnet_naming_rule.const_fields.style = all_upper
 | 
			
		||||
dotnet_naming_rule.const_fields.severity = warning
 | 
			
		||||
@@ -209,10 +217,6 @@ dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
 | 
			
		||||
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
 | 
			
		||||
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.private_field_should_be_begins_with_underscore.severity = error
 | 
			
		||||
dotnet_naming_rule.private_field_should_be_begins_with_underscore.symbols = private_field
 | 
			
		||||
dotnet_naming_rule.private_field_should_be_begins_with_underscore.style = begins_with_underscore
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
 | 
			
		||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
 | 
			
		||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
 | 
			
		||||
@@ -221,10 +225,6 @@ dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
 | 
			
		||||
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
 | 
			
		||||
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case
 | 
			
		||||
 | 
			
		||||
dotnet_naming_rule.public_anything_should_be_pascal_case.severity = error
 | 
			
		||||
dotnet_naming_rule.public_anything_should_be_pascal_case.symbols = public_anything
 | 
			
		||||
dotnet_naming_rule.public_anything_should_be_pascal_case.style = pascal_case
 | 
			
		||||
 | 
			
		||||
# Symbol specifications
 | 
			
		||||
 | 
			
		||||
dotnet_naming_symbols.const_fields.required_modifiers = const
 | 
			
		||||
@@ -262,8 +262,12 @@ dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, meth
 | 
			
		||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
 | 
			
		||||
dotnet_naming_symbols.non_field_members.required_modifiers = 
 | 
			
		||||
 | 
			
		||||
dotnet_naming_symbols.private_readonly_field.applicable_kinds = field
 | 
			
		||||
dotnet_naming_symbols.private_readonly_field.applicable_accessibilities = private
 | 
			
		||||
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
 | 
			
		||||
dotnet_naming_symbols.private_field.applicable_accessibilities = private, protected
 | 
			
		||||
dotnet_naming_symbols.private_field.required_modifiers = 
 | 
			
		||||
 | 
			
		||||
dotnet_naming_symbols.async_method.applicable_kinds = method, local_function
 | 
			
		||||
@@ -274,10 +278,6 @@ 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.public_anything.applicable_kinds = property, field, event, class, struct, interface, enum, delegate, method
 | 
			
		||||
dotnet_naming_symbols.public_anything.applicable_accessibilities = public, internal
 | 
			
		||||
dotnet_naming_symbols.public_anything.required_modifiers = 
 | 
			
		||||
 | 
			
		||||
# Naming styles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -330,8 +330,11 @@ resharper_csharp_wrap_parameters_style = chop_if_long
 | 
			
		||||
resharper_force_chop_compound_if_expression = true
 | 
			
		||||
resharper_keep_existing_linebreaks = false
 | 
			
		||||
resharper_max_formal_parameters_on_line = 3
 | 
			
		||||
resharper_wrap_chained_binary_expressions = chop_if_long
 | 
			
		||||
resharper_wrap_chained_binary_patterns = chop_if_long
 | 
			
		||||
resharper_wrap_chained_method_calls = chop_always
 | 
			
		||||
 | 
			
		||||
resharper_csharp_wrap_before_first_type_parameter_constraint=true
 | 
			
		||||
resharper_csharp_place_type_constraints_on_same_line=false
 | 
			
		||||
resharper_csharp_wrap_before_extends_colon=true
 | 
			
		||||
resharper_csharp_place_constructor_initializer_on_same_line=false
 | 
			
		||||
resharper_csharp_wrap_before_first_type_parameter_constraint = true
 | 
			
		||||
resharper_csharp_place_type_constraints_on_same_line = false
 | 
			
		||||
resharper_csharp_wrap_before_extends_colon = true
 | 
			
		||||
resharper_csharp_place_constructor_initializer_on_same_line = false
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public enum AddRemove
 | 
			
		||||
{
 | 
			
		||||
    Add = int.MinValue,
 | 
			
		||||
    Remove = int.MinValue + 1,
 | 
			
		||||
    Rem = int.MinValue + 1,
 | 
			
		||||
    Rm = int.MinValue + 1,
 | 
			
		||||
}
 | 
			
		||||
@@ -4,13 +4,15 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class AsyncLazy<T> : Lazy<Task<T>>
 | 
			
		||||
{
 | 
			
		||||
    public AsyncLazy(Func<T> valueFactory) :
 | 
			
		||||
        base(() => Task.Run(valueFactory))
 | 
			
		||||
    { }
 | 
			
		||||
    public AsyncLazy(Func<T> valueFactory)
 | 
			
		||||
        : base(() => Task.Run(valueFactory))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AsyncLazy(Func<Task<T>> taskFactory) :
 | 
			
		||||
        base(() => Task.Run(taskFactory))
 | 
			
		||||
    { }
 | 
			
		||||
    public AsyncLazy(Func<Task<T>> taskFactory)
 | 
			
		||||
        : base(() => Task.Run(taskFactory))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TaskAwaiter<T> GetAwaiter()
 | 
			
		||||
        => Value.GetAwaiter();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,7 @@ public class CmdStrings
 | 
			
		||||
    public string Description { get; }
 | 
			
		||||
 | 
			
		||||
    [JsonConstructor]
 | 
			
		||||
    public CmdStrings(
 | 
			
		||||
        [JsonProperty("args")]string[] usages,
 | 
			
		||||
        [JsonProperty("desc")]string description
 | 
			
		||||
    )
 | 
			
		||||
    public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
 | 
			
		||||
    {
 | 
			
		||||
        Usages = usages;
 | 
			
		||||
        Description = description;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,20 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.Collections;
 | 
			
		||||
 | 
			
		||||
public class IndexedCollection<T> : IList<T> where T : class, IIndexed
 | 
			
		||||
public class IndexedCollection<T> : IList<T>
 | 
			
		||||
    where T : class, IIndexed
 | 
			
		||||
{
 | 
			
		||||
    public List<T> Source { get; }
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public int Count => Source.Count;
 | 
			
		||||
    public bool IsReadOnly => false;
 | 
			
		||||
    public int IndexOf(T item) => item.Index;
 | 
			
		||||
    public int Count
 | 
			
		||||
        => Source.Count;
 | 
			
		||||
 | 
			
		||||
    public bool IsReadOnly
 | 
			
		||||
        => false;
 | 
			
		||||
 | 
			
		||||
    public int IndexOf([NotNull] T item)
 | 
			
		||||
        => item.Index;
 | 
			
		||||
 | 
			
		||||
    public IndexedCollection()
 | 
			
		||||
        => Source = new();
 | 
			
		||||
@@ -36,16 +42,17 @@ public class IndexedCollection<T> : IList<T> where T : class, IIndexed
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static implicit operator List<T>(IndexedCollection<T> x) =>
 | 
			
		||||
        x.Source;
 | 
			
		||||
    public static implicit operator List<T>(IndexedCollection<T> x)
 | 
			
		||||
        => x.Source;
 | 
			
		||||
 | 
			
		||||
    public List<T> ToList() => Source.ToList();
 | 
			
		||||
    public List<T> ToList()
 | 
			
		||||
        => Source.ToList();
 | 
			
		||||
 | 
			
		||||
    public IEnumerator<T> GetEnumerator() =>
 | 
			
		||||
        Source.GetEnumerator();
 | 
			
		||||
    public IEnumerator<T> GetEnumerator()
 | 
			
		||||
        => Source.GetEnumerator();
 | 
			
		||||
 | 
			
		||||
    IEnumerator IEnumerable.GetEnumerator() =>
 | 
			
		||||
        Source.GetEnumerator();
 | 
			
		||||
    IEnumerator IEnumerable.GetEnumerator()
 | 
			
		||||
        => Source.GetEnumerator();
 | 
			
		||||
 | 
			
		||||
    public void Add(T item)
 | 
			
		||||
    {
 | 
			
		||||
@@ -82,19 +89,21 @@ public class IndexedCollection<T> : IList<T> where T : class, IIndexed
 | 
			
		||||
 | 
			
		||||
    public virtual bool Remove(T item)
 | 
			
		||||
    {
 | 
			
		||||
        bool removed;
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if (removed = Source.Remove(item))
 | 
			
		||||
            if (Source.Remove(item))
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 0; i < Source.Count; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    if (Source[i].Index != i)
 | 
			
		||||
                        Source[i].Index = i;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return removed;
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public virtual void Insert(int index, T item)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,14 +18,16 @@ next to the response. The color depends whether the command
 | 
			
		||||
is completed, errored or in progress (pending)
 | 
			
		||||
Color settings below are for the color of those lines.
 | 
			
		||||
To get color's hex, you can go here https://htmlcolorcodes.com/
 | 
			
		||||
and copy the hex code fo your selected color (marked as #)")]
 | 
			
		||||
and copy the hex code fo your selected color (marked as #)"
 | 
			
		||||
    )]
 | 
			
		||||
    public ColorConfig Color { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
 | 
			
		||||
    public CultureInfo DefaultLocale { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Style in which executed commands will show up in the console.
 | 
			
		||||
Allowed values: Simple, Normal, None")]
 | 
			
		||||
Allowed values: Simple, Normal, None"
 | 
			
		||||
    )]
 | 
			
		||||
    public ConsoleOutputType ConsoleOutputType { get; set; }
 | 
			
		||||
 | 
			
		||||
//         [Comment(@"For what kind of updates will the bot check.
 | 
			
		||||
@@ -38,30 +40,35 @@ Allowed values: Simple, Normal, None")]
 | 
			
		||||
    [Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")]
 | 
			
		||||
    public bool ForwardMessages { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
 | 
			
		||||
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
 | 
			
		||||
    [Comment(
 | 
			
		||||
        @"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
 | 
			
		||||
or all owners? (this might cause the bot to lag if there's a lot of owners specified)"
 | 
			
		||||
    )]
 | 
			
		||||
    public bool ForwardToAllOwners { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"When a user DMs the bot with a message which is not a command
 | 
			
		||||
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
 | 
			
		||||
Supports embeds. How it looks: https://puu.sh/B0BLV.png")]
 | 
			
		||||
Supports embeds. How it looks: https://puu.sh/B0BLV.png"
 | 
			
		||||
    )]
 | 
			
		||||
    [YamlMember(ScalarStyle = ScalarStyle.Literal)]
 | 
			
		||||
    public string DmHelpText { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
 | 
			
		||||
Case insensitive.
 | 
			
		||||
Leave empty to reply with DmHelpText to every DM.")]
 | 
			
		||||
Leave empty to reply with DmHelpText to every DM."
 | 
			
		||||
    )]
 | 
			
		||||
    public List<string> DmHelpTextKeywords { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"This is the response for the .h command")]
 | 
			
		||||
    [YamlMember(ScalarStyle = ScalarStyle.Literal)]
 | 
			
		||||
    public string HelpText { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"List of modules and commands completely blocked on the bot")]
 | 
			
		||||
    public BlockedConfig Blocked { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Which string will be used to recognize the commands")]
 | 
			
		||||
    public string Prefix { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
 | 
			
		||||
1st user who joins will get greeted immediately
 | 
			
		||||
If more users join within the next 5 seconds, they will be greeted in groups of 5.
 | 
			
		||||
@@ -70,12 +77,14 @@ Keep in mind this might break some of your embeds - for example if you have %use
 | 
			
		||||
it will become invalid, as it will resolve to a list of avatars of grouped users.
 | 
			
		||||
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
 | 
			
		||||
      servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
 | 
			
		||||
      and (slightly) reduce the greet spam in those servers.")]
 | 
			
		||||
      and (slightly) reduce the greet spam in those servers."
 | 
			
		||||
    )]
 | 
			
		||||
    public bool GroupGreets { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Whether the bot will rotate through all specified statuses.
 | 
			
		||||
This setting can be changed via .rots command.
 | 
			
		||||
See RotatingStatuses submodule in Administration.")]
 | 
			
		||||
See RotatingStatuses submodule in Administration."
 | 
			
		||||
    )]
 | 
			
		||||
    public bool RotateStatuses { get; set; }
 | 
			
		||||
 | 
			
		||||
//         [Comment(@"Whether the prefix will be a suffix, or prefix.
 | 
			
		||||
@@ -159,10 +168,10 @@ public partial class ColorConfig
 | 
			
		||||
{
 | 
			
		||||
    [Comment(@"Color used for embed responses when command successfully executes")]
 | 
			
		||||
    public Rgba32 Ok { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Color used for embed responses when command has an error")]
 | 
			
		||||
    public Rgba32 Error { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Color used for embed responses while command is doing work or is in progress")]
 | 
			
		||||
    public Rgba32 Pending { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -173,7 +182,7 @@ public partial class ColorConfig
 | 
			
		||||
        Pending = Rgba32.ParseHex("faa61a");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
public enum ConsoleOutputType
 | 
			
		||||
{
 | 
			
		||||
    Normal = 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,81 +11,93 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
        OwnerIds = new List<ulong>();
 | 
			
		||||
        TotalShards = 1;
 | 
			
		||||
        GoogleApiKey = string.Empty;
 | 
			
		||||
        Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
 | 
			
		||||
        Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
 | 
			
		||||
        Votes = new(string.Empty,
 | 
			
		||||
            string.Empty,
 | 
			
		||||
            string.Empty,
 | 
			
		||||
            string.Empty
 | 
			
		||||
        );
 | 
			
		||||
        Patreon = new(string.Empty,
 | 
			
		||||
            string.Empty,
 | 
			
		||||
            string.Empty,
 | 
			
		||||
            string.Empty
 | 
			
		||||
        );
 | 
			
		||||
        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";
 | 
			
		||||
 | 
			
		||||
        RestartCommand = new()
 | 
			
		||||
        {
 | 
			
		||||
        };
 | 
			
		||||
        RestartCommand = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"DO NOT CHANGE")]
 | 
			
		||||
    public int Version { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")]
 | 
			
		||||
    public string Token { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"List of Ids of the users who have bot owner permissions
 | 
			
		||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**")]
 | 
			
		||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**"
 | 
			
		||||
    )]
 | 
			
		||||
    public ICollection<ulong> OwnerIds { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"The number of shards that the bot will running on.
 | 
			
		||||
Leave at 1 if you don't know what you're doing.")]
 | 
			
		||||
Leave at 1 if you don't know what you're doing."
 | 
			
		||||
    )]
 | 
			
		||||
    public int TotalShards { get; set; }
 | 
			
		||||
        
 | 
			
		||||
    [Comment(@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
 | 
			
		||||
 | 
			
		||||
    [Comment(
 | 
			
		||||
        @"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
 | 
			
		||||
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
 | 
			
		||||
Used only for Youtube Data Api (at the moment).")]
 | 
			
		||||
Used only for Youtube Data Api (at the moment)."
 | 
			
		||||
    )]
 | 
			
		||||
    public string GoogleApiKey { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
 | 
			
		||||
    public VotesSettings Votes { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Patreon auto reward system settings.
 | 
			
		||||
go to https://www.patreon.com/portal -> my clients -> create client")]
 | 
			
		||||
go to https://www.patreon.com/portal -> my clients -> create client"
 | 
			
		||||
    )]
 | 
			
		||||
    public PatreonSettings Patreon { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Api key for sending stats to DiscordBotList.")]
 | 
			
		||||
    public string BotListToken { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Official cleverbot api key.")]
 | 
			
		||||
    public string CleverbotApiKey { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Redis connection string. Don't change if you don't know what you're doing.")]
 | 
			
		||||
    public string RedisOptions { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")]
 | 
			
		||||
    public DbOptions Db { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Address and port of the coordinator endpoint. Leave empty for default.
 | 
			
		||||
Change only if you've changed the coordinator address or port.")]
 | 
			
		||||
Change only if you've changed the coordinator address or port."
 | 
			
		||||
    )]
 | 
			
		||||
    public string CoordinatorUrl { get; set; }
 | 
			
		||||
        
 | 
			
		||||
    [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)"
 | 
			
		||||
    )]
 | 
			
		||||
    public string RapidApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"https://locationiq.com api key (register and you will receive the token in the email).
 | 
			
		||||
Used only for .time command.")]
 | 
			
		||||
Used only for .time command."
 | 
			
		||||
    )]
 | 
			
		||||
    public string LocationIqApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"https://timezonedb.com api key (register and you will receive the token in the email).
 | 
			
		||||
Used only for .time command")]
 | 
			
		||||
Used only for .time command"
 | 
			
		||||
    )]
 | 
			
		||||
    public string TimezoneDbApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
 | 
			
		||||
Used for cryptocurrency related commands.")]
 | 
			
		||||
Used for cryptocurrency related commands."
 | 
			
		||||
    )]
 | 
			
		||||
    public string CoinmarketcapApiKey { get; set; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
 | 
			
		||||
    public string OsuApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +111,8 @@ Linux default
 | 
			
		||||
    args: ""NadekoBot.dll -- {0}""
 | 
			
		||||
Windows default
 | 
			
		||||
    cmd: NadekoBot.exe
 | 
			
		||||
    args: {0}")]
 | 
			
		||||
    args: {0}"
 | 
			
		||||
    )]
 | 
			
		||||
    public RestartConfig RestartCommand { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -107,6 +120,7 @@ Windows default
 | 
			
		||||
    {
 | 
			
		||||
        [Comment(@"Database type. Only sqlite supported atm")]
 | 
			
		||||
        public string Type { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Connection string. Will default to ""Data Source=data/NadekoBot.db""")]
 | 
			
		||||
        public string ConnectionString { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
@@ -118,10 +132,16 @@ Windows default
 | 
			
		||||
        public string RefreshToken { get; set; }
 | 
			
		||||
        public string ClientSecret { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
 | 
			
		||||
        [Comment(
 | 
			
		||||
            @"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)"
 | 
			
		||||
        )]
 | 
			
		||||
        public string CampaignId { get; set; }
 | 
			
		||||
 | 
			
		||||
        public PatreonSettings(string accessToken, string refreshToken, string clientSecret, string campaignId)
 | 
			
		||||
        public PatreonSettings(
 | 
			
		||||
            string accessToken,
 | 
			
		||||
            string refreshToken,
 | 
			
		||||
            string clientSecret,
 | 
			
		||||
            string campaignId)
 | 
			
		||||
        {
 | 
			
		||||
            AccessToken = accessToken;
 | 
			
		||||
            RefreshToken = refreshToken;
 | 
			
		||||
@@ -131,7 +151,6 @@ Windows default
 | 
			
		||||
 | 
			
		||||
        public PatreonSettings()
 | 
			
		||||
        {
 | 
			
		||||
                
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -139,28 +158,35 @@ Windows default
 | 
			
		||||
    {
 | 
			
		||||
        [Comment(@"top.gg votes service url
 | 
			
		||||
This is the url of your instance of the NadekoBot.Votes api
 | 
			
		||||
Example: https://votes.my.cool.bot.com")]
 | 
			
		||||
Example: https://votes.my.cool.bot.com"
 | 
			
		||||
        )]
 | 
			
		||||
        public string TopggServiceUrl { get; set; }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Authorization header value sent to the TopGG service url with each request
 | 
			
		||||
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")]
 | 
			
		||||
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file"
 | 
			
		||||
        )]
 | 
			
		||||
        public string TopggKey { get; set; }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        [Comment(@"discords.com votes service url
 | 
			
		||||
This is the url of your instance of the NadekoBot.Votes api
 | 
			
		||||
Example: https://votes.my.cool.bot.com")]
 | 
			
		||||
Example: https://votes.my.cool.bot.com"
 | 
			
		||||
        )]
 | 
			
		||||
        public string DiscordsServiceUrl { get; set; }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Authorization header value sent to the Discords service url with each request
 | 
			
		||||
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")]
 | 
			
		||||
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file"
 | 
			
		||||
        )]
 | 
			
		||||
        public string DiscordsKey { get; set; }
 | 
			
		||||
 | 
			
		||||
        public VotesSettings()
 | 
			
		||||
        {
 | 
			
		||||
                
 | 
			
		||||
        }
 | 
			
		||||
            
 | 
			
		||||
        public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey)
 | 
			
		||||
 | 
			
		||||
        public VotesSettings(
 | 
			
		||||
            string topggServiceUrl,
 | 
			
		||||
            string topggKey,
 | 
			
		||||
            string discordsServiceUrl,
 | 
			
		||||
            string discordsKey)
 | 
			
		||||
        {
 | 
			
		||||
            TopggServiceUrl = topggServiceUrl;
 | 
			
		||||
            TopggKey = topggKey;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
public class DownloadTracker : INService
 | 
			
		||||
{
 | 
			
		||||
    private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
 | 
			
		||||
    private readonly SemaphoreSlim downloadUsersSemaphore = new(1, 1);
 | 
			
		||||
    private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Ensures all users on the specified guild were downloaded within the last hour. 
 | 
			
		||||
@@ -12,16 +12,16 @@ public class DownloadTracker : INService
 | 
			
		||||
    /// <returns>Task representing download state</returns>
 | 
			
		||||
    public async Task EnsureUsersDownloadedAsync(IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        await downloadUsersSemaphore.WaitAsync();
 | 
			
		||||
        await _downloadUsersSemaphore.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
            // download once per hour at most
 | 
			
		||||
            var added = LastDownloads.AddOrUpdate(
 | 
			
		||||
                guild.Id,
 | 
			
		||||
            var added = LastDownloads.AddOrUpdate(guild.Id,
 | 
			
		||||
                now,
 | 
			
		||||
                (key, old) => now - old > TimeSpan.FromHours(1) ? now : old);
 | 
			
		||||
                (_, old) => now - old > TimeSpan.FromHours(1) ? now : old
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // means that this entry was just added - download the users
 | 
			
		||||
            if (added == now)
 | 
			
		||||
@@ -29,7 +29,7 @@ public class DownloadTracker : INService
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            downloadUsersSemaphore.Release();
 | 
			
		||||
            _downloadUsersSemaphore.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ public static class Helpers
 | 
			
		||||
    {
 | 
			
		||||
        if (!Console.IsInputRedirected)
 | 
			
		||||
            Console.ReadKey();
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        Environment.Exit(exitCode);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,7 +21,7 @@ public interface IBotCredentials
 | 
			
		||||
    string CoinmarketcapApiKey { get; }
 | 
			
		||||
    string CoordinatorUrl { get; set; }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
public class RestartConfig
 | 
			
		||||
{
 | 
			
		||||
    public string Cmd { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public interface ICloneable<T> where T : new()
 | 
			
		||||
public interface ICloneable<T>
 | 
			
		||||
    where T : new()
 | 
			
		||||
{
 | 
			
		||||
    public T Clone();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +1,13 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.JsonConverters;
 | 
			
		||||
 | 
			
		||||
public class Rgba32Converter : JsonConverter<Rgba32>
 | 
			
		||||
{
 | 
			
		||||
    public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
        => Rgba32.ParseHex(reader.GetString());
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
 | 
			
		||||
        => writer.WriteStringValue(value.ToHex());
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
public class CultureInfoConverter : JsonConverter<CultureInfo>
 | 
			
		||||
{
 | 
			
		||||
    public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
        => new(reader.GetString());
 | 
			
		||||
        => new(reader.GetString() ?? "en-US");
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
 | 
			
		||||
        => writer.WriteStringValue(value.Name);
 | 
			
		||||
							
								
								
									
										14
									
								
								src/NadekoBot/Common/JsonConverters/Rgba32Converter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/NadekoBot/Common/JsonConverters/Rgba32Converter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.JsonConverters;
 | 
			
		||||
 | 
			
		||||
public class Rgba32Converter : JsonConverter<Rgba32>
 | 
			
		||||
{
 | 
			
		||||
    public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
        => Rgba32.ParseHex(reader.GetString());
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
 | 
			
		||||
        => writer.WriteStringValue(value.ToHex());
 | 
			
		||||
}
 | 
			
		||||
@@ -4,14 +4,17 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
// needs proper invalid input check (character array input out of range)
 | 
			
		||||
// needs negative number support
 | 
			
		||||
// ReSharper disable once InconsistentNaming
 | 
			
		||||
#pragma warning disable IDE1006
 | 
			
		||||
public readonly struct kwum : IEquatable<kwum>
 | 
			
		||||
#pragma warning restore IDE1006
 | 
			
		||||
{
 | 
			
		||||
    private readonly int _value;
 | 
			
		||||
    private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz";
 | 
			
		||||
    private const string VALID_CHARACTERS = "23456789abcdefghijkmnpqrstuvwxyz";
 | 
			
		||||
 | 
			
		||||
    public kwum(int num)
 | 
			
		||||
        => _value = num;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public kwum(in char c)
 | 
			
		||||
    {
 | 
			
		||||
        if (!IsValidChar(c))
 | 
			
		||||
@@ -21,11 +24,11 @@ public readonly struct kwum : IEquatable<kwum>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private static int InternalCharToValue(in char c) 
 | 
			
		||||
        => ValidCharacters.IndexOf(c);
 | 
			
		||||
    private static int InternalCharToValue(in char c)
 | 
			
		||||
        => VALID_CHARACTERS.IndexOf(c);
 | 
			
		||||
 | 
			
		||||
    public kwum(in ReadOnlySpan<char> input)
 | 
			
		||||
    {;
 | 
			
		||||
    {
 | 
			
		||||
        _value = 0;
 | 
			
		||||
        for (var index = 0; index < input.Length; index++)
 | 
			
		||||
        {
 | 
			
		||||
@@ -33,14 +36,14 @@ public readonly struct kwum : IEquatable<kwum>
 | 
			
		||||
            if (!IsValidChar(c))
 | 
			
		||||
                throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input));
 | 
			
		||||
 | 
			
		||||
            _value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1);
 | 
			
		||||
            _value += VALID_CHARACTERS.IndexOf(c) * (int)Math.Pow(VALID_CHARACTERS.Length, input.Length - index - 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool TryParse(in ReadOnlySpan<char> input, out kwum value)
 | 
			
		||||
    {
 | 
			
		||||
        value = default;
 | 
			
		||||
        foreach(var c in input)
 | 
			
		||||
        foreach (var c in input)
 | 
			
		||||
            if (!IsValidChar(c))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
@@ -59,25 +62,26 @@ public readonly struct kwum : IEquatable<kwum>
 | 
			
		||||
 | 
			
		||||
    public static implicit operator long(kwum kwum)
 | 
			
		||||
        => kwum._value;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static implicit operator int(kwum kwum)
 | 
			
		||||
        => kwum._value;
 | 
			
		||||
 | 
			
		||||
    public static implicit operator kwum(int num)
 | 
			
		||||
        => new(num);
 | 
			
		||||
 | 
			
		||||
    public static bool IsValidChar(char c)
 | 
			
		||||
        => ValidCharacters.Contains(c);
 | 
			
		||||
        => VALID_CHARACTERS.Contains(c);
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
    {
 | 
			
		||||
        var count = ValidCharacters.Length;
 | 
			
		||||
        var count = VALID_CHARACTERS.Length;
 | 
			
		||||
        var localValue = _value;
 | 
			
		||||
        var arrSize = (int)Math.Log(localValue, count) + 1;
 | 
			
		||||
        Span<char> chars = new char[arrSize];
 | 
			
		||||
        while (localValue > 0)
 | 
			
		||||
        {
 | 
			
		||||
            localValue = Math.DivRem(localValue, count, out var rem);
 | 
			
		||||
            chars[--arrSize] = ValidCharacters[rem];
 | 
			
		||||
            chars[--arrSize] = VALID_CHARACTERS[rem];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new(chars);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,14 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class LbOpts : INadekoCommandOptions
 | 
			
		||||
{
 | 
			
		||||
    [Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
 | 
			
		||||
    [Option('c',
 | 
			
		||||
        "clean",
 | 
			
		||||
        Default = false,
 | 
			
		||||
        HelpText = "Only show users who are on the server."
 | 
			
		||||
    )]
 | 
			
		||||
    public bool Clean { get; set; }
 | 
			
		||||
 | 
			
		||||
    public void NormalizeOptions()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,18 +17,21 @@ public class LoginErrorHandler
 | 
			
		||||
            case HttpStatusCode.Unauthorized:
 | 
			
		||||
                Log.Error("Your bot token is wrong.\n" +
 | 
			
		||||
                          "You can find the bot token under the Bot tab in the developer page.\n" +
 | 
			
		||||
                          "Fix your token in the credentials file and restart the bot");
 | 
			
		||||
                          "Fix your token in the credentials file and restart the bot"
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case HttpStatusCode.BadRequest:
 | 
			
		||||
                Log.Error("Something has been incorrectly formatted in your credentials file.\n" +
 | 
			
		||||
                          "Use the JSON Guide as reference to fix it and restart the bot.");
 | 
			
		||||
                          "Use the JSON Guide as reference to fix it and restart the bot"
 | 
			
		||||
                );
 | 
			
		||||
                Log.Error("If you are on Linux, make sure Redis is installed and running");
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case HttpStatusCode.RequestTimeout:
 | 
			
		||||
                Log.Error("The request timed out. Make sure you have no external program blocking the bot " +
 | 
			
		||||
                          "from connecting to the internet");
 | 
			
		||||
                          "from connecting to the internet"
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case HttpStatusCode.ServiceUnavailable:
 | 
			
		||||
@@ -38,7 +41,8 @@ public class LoginErrorHandler
 | 
			
		||||
 | 
			
		||||
            case HttpStatusCode.TooManyRequests:
 | 
			
		||||
                Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" +
 | 
			
		||||
                          "Global ratelimits usually last for an hour");
 | 
			
		||||
                          "Global ratelimits usually last for an hour"
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
@@ -46,6 +50,6 @@ public class LoginErrorHandler
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Log.Fatal(ex.ToString());
 | 
			
		||||
        Log.Fatal(ex, "Fatal error occurred while loading credentials");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
public interface IInputTransformer
 | 
			
		||||
{
 | 
			
		||||
    Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/NadekoBot/Common/ModuleBehaviors/IInputTransformer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/NadekoBot/Common/ModuleBehaviors/IInputTransformer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
public interface IInputTransformer
 | 
			
		||||
{
 | 
			
		||||
    Task<string> TransformInput(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessageChannel channel,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        string input);
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
public interface ILateBlocker
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,60 +1,78 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
// ReSharper disable InconsistentNaming
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
[UsedImplicitly(ImplicitUseTargetFlags.Default
 | 
			
		||||
                | ImplicitUseTargetFlags.WithInheritors
 | 
			
		||||
                | ImplicitUseTargetFlags.WithMembers)]
 | 
			
		||||
[UsedImplicitly(ImplicitUseTargetFlags.Default |
 | 
			
		||||
                ImplicitUseTargetFlags.WithInheritors |
 | 
			
		||||
                ImplicitUseTargetFlags.WithMembers
 | 
			
		||||
)]
 | 
			
		||||
public abstract class NadekoModule : ModuleBase
 | 
			
		||||
{
 | 
			
		||||
    protected CultureInfo _cultureInfo { get; set; }
 | 
			
		||||
    protected CultureInfo Culture { get; set; }
 | 
			
		||||
    public IBotStrings Strings { get; set; }
 | 
			
		||||
    public CommandHandler CmdHandler { get; set; }
 | 
			
		||||
    public ILocalization Localization { get; set; }
 | 
			
		||||
    public IEmbedBuilderService _eb { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Prefix => CmdHandler.GetPrefix(ctx.Guild);
 | 
			
		||||
    public string Prefix
 | 
			
		||||
        => CmdHandler.GetPrefix(ctx.Guild);
 | 
			
		||||
 | 
			
		||||
    protected ICommandContext ctx => Context;
 | 
			
		||||
 | 
			
		||||
    protected NadekoModule()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    protected ICommandContext ctx
 | 
			
		||||
        => Context;
 | 
			
		||||
 | 
			
		||||
    protected override void BeforeExecute(CommandInfo cmd)
 | 
			
		||||
        => _cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id);
 | 
			
		||||
        => Culture = Localization.GetCultureInfo(ctx.Guild?.Id);
 | 
			
		||||
 | 
			
		||||
    protected string GetText(in LocStr data) =>
 | 
			
		||||
        Strings.GetText(data, _cultureInfo);
 | 
			
		||||
    protected string GetText(in LocStr data)
 | 
			
		||||
        => Strings.GetText(data, Culture);
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> SendErrorAsync(string error)
 | 
			
		||||
        => ctx.Channel.SendErrorAsync(_eb, error);
 | 
			
		||||
        
 | 
			
		||||
    public Task<IUserMessage> SendErrorAsync(string title, string error, string url = null, string footer = null)
 | 
			
		||||
        => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> SendErrorAsync(
 | 
			
		||||
        string title,
 | 
			
		||||
        string error,
 | 
			
		||||
        string url = null,
 | 
			
		||||
        string footer = null)
 | 
			
		||||
        => ctx.Channel.SendErrorAsync(_eb,
 | 
			
		||||
            title,
 | 
			
		||||
            error,
 | 
			
		||||
            url,
 | 
			
		||||
            footer
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(string text)
 | 
			
		||||
        => ctx.Channel.SendConfirmAsync(_eb, text);
 | 
			
		||||
        
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(string title, string text, string url = null, string footer = null)
 | 
			
		||||
        => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(
 | 
			
		||||
        string title,
 | 
			
		||||
        string text,
 | 
			
		||||
        string url = null,
 | 
			
		||||
        string footer = null)
 | 
			
		||||
        => ctx.Channel.SendConfirmAsync(_eb,
 | 
			
		||||
            title,
 | 
			
		||||
            text,
 | 
			
		||||
            url,
 | 
			
		||||
            footer
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> SendPendingAsync(string text)
 | 
			
		||||
        => ctx.Channel.SendPendingAsync(_eb, text);
 | 
			
		||||
        
 | 
			
		||||
    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str) 
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
 | 
			
		||||
        => SendErrorAsync(GetText(str));
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> PendingLocalizedAsync(LocStr str) 
 | 
			
		||||
    public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
 | 
			
		||||
        => SendPendingAsync(GetText(str));
 | 
			
		||||
        
 | 
			
		||||
    public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str) 
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
 | 
			
		||||
        => SendConfirmAsync(GetText(str));
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str) 
 | 
			
		||||
    public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
 | 
			
		||||
        => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str) 
 | 
			
		||||
    public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
 | 
			
		||||
        => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
 | 
			
		||||
@@ -62,17 +80,19 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
 | 
			
		||||
    {
 | 
			
		||||
        embed
 | 
			
		||||
            .WithPendingColor()
 | 
			
		||||
        embed.WithPendingColor()
 | 
			
		||||
            .WithFooter("yes/no");
 | 
			
		||||
 | 
			
		||||
        var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
        var msg = await ctx.Channel.EmbedAsync(embed)
 | 
			
		||||
            .ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false);
 | 
			
		||||
            var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id)
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
            input = input?.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
            if (input != "YES" && input != "Y")
 | 
			
		||||
            if (input != "YES" &&
 | 
			
		||||
                input != "Y")
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
@@ -94,7 +114,9 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
        {
 | 
			
		||||
            dsc.MessageReceived += MessageReceived;
 | 
			
		||||
 | 
			
		||||
            if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false) != userInputTask.Task)
 | 
			
		||||
            if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000))
 | 
			
		||||
                    .ConfigureAwait(false) !=
 | 
			
		||||
                userInputTask.Task)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
@@ -109,21 +131,23 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
        Task MessageReceived(SocketMessage arg)
 | 
			
		||||
        {
 | 
			
		||||
            var _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                if (arg is not SocketUserMessage userMsg ||
 | 
			
		||||
                    userMsg.Channel is not ITextChannel chan ||
 | 
			
		||||
                    userMsg.Author.Id != userId ||
 | 
			
		||||
                    userMsg.Channel.Id != channelId)
 | 
			
		||||
                {
 | 
			
		||||
                    if (arg is not SocketUserMessage userMsg ||
 | 
			
		||||
                        userMsg.Channel is not ITextChannel ||
 | 
			
		||||
                        userMsg.Author.Id != userId ||
 | 
			
		||||
                        userMsg.Channel.Id != channelId)
 | 
			
		||||
                    {
 | 
			
		||||
                        return Task.CompletedTask;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (userInputTask.TrySetResult(arg.Content))
 | 
			
		||||
                    {
 | 
			
		||||
                        userMsg.DeleteAfter(1);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (userInputTask.TrySetResult(arg.Content))
 | 
			
		||||
                {
 | 
			
		||||
                    userMsg.DeleteAfter(1);
 | 
			
		||||
                }
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            });
 | 
			
		||||
            );
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -132,20 +156,12 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
public abstract class NadekoModule<TService> : NadekoModule
 | 
			
		||||
{
 | 
			
		||||
    public TService _service { get; set; }
 | 
			
		||||
 | 
			
		||||
    protected NadekoModule() : base()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public abstract class NadekoSubmodule : NadekoModule
 | 
			
		||||
{
 | 
			
		||||
    protected NadekoSubmodule() : base() { }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public abstract class NadekoSubmodule<TService> : NadekoModule<TService>
 | 
			
		||||
{
 | 
			
		||||
    protected NadekoSubmodule() : base()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
public static class NadekoModuleExtensions
 | 
			
		||||
{
 | 
			
		||||
        
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@ public class NadekoRandom : Random
 | 
			
		||||
{
 | 
			
		||||
    private readonly RandomNumberGenerator _rng;
 | 
			
		||||
 | 
			
		||||
    public NadekoRandom() : base()
 | 
			
		||||
    public NadekoRandom()
 | 
			
		||||
        : base()
 | 
			
		||||
        => _rng = RandomNumberGenerator.Create();
 | 
			
		||||
 | 
			
		||||
    public override int Next()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
 | 
			
		||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
 | 
			
		||||
public sealed class NoPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
{
 | 
			
		||||
    public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
            return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
 | 
			
		||||
#else
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,18 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public static class OptionsParser
 | 
			
		||||
{
 | 
			
		||||
    public static T ParseFrom<T>(string[] args) where T : INadekoCommandOptions, new()
 | 
			
		||||
    public static T ParseFrom<T>(string[] args)
 | 
			
		||||
        where T : INadekoCommandOptions, new()
 | 
			
		||||
        => ParseFrom(new T(), args).Item1;
 | 
			
		||||
 | 
			
		||||
    public static (T, bool) ParseFrom<T>(T options, string[] args) where T : INadekoCommandOptions
 | 
			
		||||
    public static (T, bool) ParseFrom<T>(T options, string[] args)
 | 
			
		||||
        where T : INadekoCommandOptions
 | 
			
		||||
    {
 | 
			
		||||
        using var p = new Parser(x =>
 | 
			
		||||
        {
 | 
			
		||||
            x.HelpWriter = null;
 | 
			
		||||
        });
 | 
			
		||||
            {
 | 
			
		||||
                x.HelpWriter = null;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        var res = p.ParseArguments<T>(args);
 | 
			
		||||
        options = res.MapResult(x => x, x => options);
 | 
			
		||||
        options.NormalizeOptions();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,37 +4,54 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class OsuUserBests
 | 
			
		||||
{
 | 
			
		||||
    [JsonProperty("beatmap_id")] public string BeatmapId { get; set; }
 | 
			
		||||
    [JsonProperty("beatmap_id")]
 | 
			
		||||
    public string BeatmapId { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("score_id")] public string ScoreId { get; set; }
 | 
			
		||||
    [JsonProperty("score_id")]
 | 
			
		||||
    public string ScoreId { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("score")] public string Score { get; set; }
 | 
			
		||||
    [JsonProperty("score")]
 | 
			
		||||
    public string Score { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("maxcombo")] public string Maxcombo { get; set; }
 | 
			
		||||
    [JsonProperty("maxcombo")]
 | 
			
		||||
    public string Maxcombo { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("count50")] public double Count50 { get; set; }
 | 
			
		||||
    [JsonProperty("count50")]
 | 
			
		||||
    public double Count50 { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("count100")] public double Count100 { get; set; }
 | 
			
		||||
    [JsonProperty("count100")]
 | 
			
		||||
    public double Count100 { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("count300")] public double Count300 { get; set; }
 | 
			
		||||
    [JsonProperty("count300")]
 | 
			
		||||
    public double Count300 { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("countmiss")] public int Countmiss { get; set; }
 | 
			
		||||
    [JsonProperty("countmiss")]
 | 
			
		||||
    public int Countmiss { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("countkatu")] public double Countkatu { get; set; }
 | 
			
		||||
    [JsonProperty("countkatu")]
 | 
			
		||||
    public double Countkatu { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("countgeki")] public double Countgeki { get; set; }
 | 
			
		||||
    [JsonProperty("countgeki")]
 | 
			
		||||
    public double Countgeki { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("perfect")] public string Perfect { get; set; }
 | 
			
		||||
    [JsonProperty("perfect")]
 | 
			
		||||
    public string Perfect { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("enabled_mods")] public int EnabledMods { get; set; }
 | 
			
		||||
    [JsonProperty("enabled_mods")]
 | 
			
		||||
    public int EnabledMods { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("user_id")] public string UserId { get; set; }
 | 
			
		||||
    [JsonProperty("user_id")]
 | 
			
		||||
    public string UserId { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("date")] public string Date { get; set; }
 | 
			
		||||
    [JsonProperty("date")]
 | 
			
		||||
    public string Date { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("rank")] public string Rank { get; set; }
 | 
			
		||||
    [JsonProperty("rank")]
 | 
			
		||||
    public string Rank { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("pp")] public double Pp { get; set; }
 | 
			
		||||
    [JsonProperty("pp")]
 | 
			
		||||
    public double Pp { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("replay_available")] public string ReplayAvailable { get; set; }
 | 
			
		||||
    [JsonProperty("replay_available")]
 | 
			
		||||
    public string ReplayAvailable { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,21 +2,24 @@
 | 
			
		||||
 | 
			
		||||
public static class PlatformHelper
 | 
			
		||||
{
 | 
			
		||||
    private const int ProcessorCountRefreshIntervalMs = 30000;
 | 
			
		||||
    private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000;
 | 
			
		||||
 | 
			
		||||
    private static volatile int _processorCount;
 | 
			
		||||
    private static volatile int _lastProcessorCountRefreshTicks;
 | 
			
		||||
    private static volatile int processorCount;
 | 
			
		||||
    private static volatile int lastProcessorCountRefreshTicks;
 | 
			
		||||
 | 
			
		||||
    public static int ProcessorCount {
 | 
			
		||||
        get {
 | 
			
		||||
    public static int ProcessorCount
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var now = Environment.TickCount;
 | 
			
		||||
            if (_processorCount == 0 || now - _lastProcessorCountRefreshTicks >= ProcessorCountRefreshIntervalMs)
 | 
			
		||||
            if (processorCount == 0 ||
 | 
			
		||||
                now - lastProcessorCountRefreshTicks >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS)
 | 
			
		||||
            {
 | 
			
		||||
                _processorCount = Environment.ProcessorCount;
 | 
			
		||||
                _lastProcessorCountRefreshTicks = now;
 | 
			
		||||
                processorCount = Environment.ProcessorCount;
 | 
			
		||||
                lastProcessorCountRefreshTicks = now;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _processorCount;
 | 
			
		||||
            return processorCount;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,19 +12,21 @@ public class SearchPokemon
 | 
			
		||||
 | 
			
		||||
    public class BaseStatsClass
 | 
			
		||||
    {
 | 
			
		||||
        public int HP { get; set; }
 | 
			
		||||
        public int ATK { get; set; }
 | 
			
		||||
        public int DEF { get; set; }
 | 
			
		||||
        public int SPA { get; set; }
 | 
			
		||||
        public int SPD { get; set; }
 | 
			
		||||
        public int SPE { get; set; }
 | 
			
		||||
        public int Hp { get; set; }
 | 
			
		||||
        public int Atk { get; set; }
 | 
			
		||||
        public int Def { get; set; }
 | 
			
		||||
        public int Spa { get; set; }
 | 
			
		||||
        public int Spd { get; set; }
 | 
			
		||||
        public int Spe { get; set; }
 | 
			
		||||
 | 
			
		||||
        public override string ToString() => $@"💚**HP:**  {HP,-4} ⚔**ATK:** {ATK,-4} 🛡**DEF:** {DEF,-4}
 | 
			
		||||
✨**SPA:** {SPA,-4} 🎇**SPD:** {SPD,-4} 💨**SPE:** {SPE,-4}";
 | 
			
		||||
        public override string ToString()
 | 
			
		||||
            => $@"💚**HP:**  {Hp,-4} ⚔**ATK:** {Atk,-4} 🛡**DEF:** {Def,-4}
 | 
			
		||||
✨**SPA:** {Spa,-4} 🎇**SPD:** {Spd,-4} 💨**SPE:** {Spe,-4}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty("num")]
 | 
			
		||||
    public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Species { get; set; }
 | 
			
		||||
    public string[] Types { get; set; }
 | 
			
		||||
    public GenderRatioClass GenderRatio { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,22 +3,20 @@
 | 
			
		||||
public class EventPubSub : IPubSub
 | 
			
		||||
{
 | 
			
		||||
    private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions = new();
 | 
			
		||||
    private readonly object locker = new();
 | 
			
		||||
        
 | 
			
		||||
    private readonly object _locker = new();
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
    {
 | 
			
		||||
        Func<object, ValueTask> localAction = obj => action((TData) obj);
 | 
			
		||||
        lock(locker)
 | 
			
		||||
        Func<object, ValueTask> localAction = obj => action((TData)obj);
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            Dictionary<Delegate, List<Func<object, ValueTask>>> keyActions;
 | 
			
		||||
            if (!_actions.TryGetValue(key.Key, out keyActions))
 | 
			
		||||
            if (!_actions.TryGetValue(key.Key, out var keyActions))
 | 
			
		||||
            {
 | 
			
		||||
                keyActions = new();
 | 
			
		||||
                _actions[key.Key] = keyActions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            List<Func<object, ValueTask>> sameActions;
 | 
			
		||||
            if (!keyActions.TryGetValue(action, out sameActions))
 | 
			
		||||
            if (!keyActions.TryGetValue(action, out var sameActions))
 | 
			
		||||
            {
 | 
			
		||||
                sameActions = new();
 | 
			
		||||
                keyActions[action] = sameActions;
 | 
			
		||||
@@ -29,19 +27,17 @@ public class EventPubSub : IPubSub
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
    {
 | 
			
		||||
        lock (locker)
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            if(_actions.TryGetValue(key.Key, out var actions))
 | 
			
		||||
            if (_actions.TryGetValue(key.Key, out var actions))
 | 
			
		||||
            {
 | 
			
		||||
                // if this class ever gets used, this needs to be properly implemented
 | 
			
		||||
                // 1. ignore all valuetasks which are completed
 | 
			
		||||
                // 2. return task.whenall all other tasks
 | 
			
		||||
                return Task.WhenAll(actions
 | 
			
		||||
                    .SelectMany(kvp => kvp.Value)
 | 
			
		||||
                    .Select(action => action(data).AsTask()));
 | 
			
		||||
                return Task.WhenAll(actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
@@ -50,12 +46,11 @@ public class EventPubSub : IPubSub
 | 
			
		||||
 | 
			
		||||
    public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
    {
 | 
			
		||||
        lock (locker)
 | 
			
		||||
        lock (_locker)
 | 
			
		||||
        {
 | 
			
		||||
            // get subscriptions for this action
 | 
			
		||||
            if (_actions.TryGetValue(key.Key, out var actions))
 | 
			
		||||
            {
 | 
			
		||||
                var hashCode = action.GetHashCode();
 | 
			
		||||
                // get subscriptions which have the same action hash code
 | 
			
		||||
                // note: having this as a list allows for multiple subscriptions of
 | 
			
		||||
                //       the same insance's/static method
 | 
			
		||||
@@ -63,13 +58,13 @@ public class EventPubSub : IPubSub
 | 
			
		||||
                {
 | 
			
		||||
                    // remove last subscription
 | 
			
		||||
                    sameActions.RemoveAt(sameActions.Count - 1);
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                    // if the last subscription was the only subscription
 | 
			
		||||
                    // we can safely remove this action's dictionary entry
 | 
			
		||||
                    if (sameActions.Count == 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        actions.Remove(action);
 | 
			
		||||
                            
 | 
			
		||||
 | 
			
		||||
                        // if our dictionary has no more elements after 
 | 
			
		||||
                        // removing the entry
 | 
			
		||||
                        // it's safe to remove it from the key's subscriptions
 | 
			
		||||
 
 | 
			
		||||
@@ -5,23 +5,19 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class JsonSeria : ISeria
 | 
			
		||||
{
 | 
			
		||||
    private readonly JsonSerializerOptions serializerOptions = new()
 | 
			
		||||
    private readonly JsonSerializerOptions _serializerOptions = new()
 | 
			
		||||
    {
 | 
			
		||||
        Converters =
 | 
			
		||||
        {
 | 
			
		||||
            new Rgba32Converter(),
 | 
			
		||||
            new CultureInfoConverter(),
 | 
			
		||||
        }
 | 
			
		||||
        Converters = { new Rgba32Converter(), new CultureInfoConverter(), }
 | 
			
		||||
    };
 | 
			
		||||
    public byte[] Serialize<T>(T data) 
 | 
			
		||||
        => JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions);
 | 
			
		||||
 | 
			
		||||
    public byte[] Serialize<T>(T data)
 | 
			
		||||
        => JsonSerializer.SerializeToUtf8Bytes(data, _serializerOptions);
 | 
			
		||||
 | 
			
		||||
    public T Deserialize<T>(byte[] data)
 | 
			
		||||
    {
 | 
			
		||||
        if (data is null)
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
        return JsonSerializer.Deserialize<T>(data, serializerOptions);
 | 
			
		||||
        return JsonSerializer.Deserialize<T>(data, _serializerOptions);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,13 +18,15 @@ public sealed class RedisPubSub : IPubSub
 | 
			
		||||
    public Task Pub<TData>(in TypedKey<TData> key, TData data)
 | 
			
		||||
    {
 | 
			
		||||
        var serialized = _serializer.Serialize(data);
 | 
			
		||||
        return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
 | 
			
		||||
        return _multi.GetSubscriber()
 | 
			
		||||
            .PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
 | 
			
		||||
    {
 | 
			
		||||
        var eventName = key.Key;
 | 
			
		||||
        return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) =>
 | 
			
		||||
 | 
			
		||||
        async void OnSubscribeHandler(RedisChannel _, RedisValue data)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -33,8 +35,10 @@ public sealed class RedisPubSub : IPubSub
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error($"Error handling the event {eventName}: {ex.Message}");
 | 
			
		||||
                Log.Error("Error handling the event {EventName}: {ErrorMessage}", eventName, ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", OnSubscribeHandler);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,18 +9,22 @@ public readonly struct TypedKey<TData>
 | 
			
		||||
 | 
			
		||||
    public static implicit operator TypedKey<TData>(in string input)
 | 
			
		||||
        => new(input);
 | 
			
		||||
 | 
			
		||||
    public static implicit operator string(in TypedKey<TData> input)
 | 
			
		||||
        => input.Key;
 | 
			
		||||
 | 
			
		||||
    public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
 | 
			
		||||
        => left.Key == right.Key;
 | 
			
		||||
 | 
			
		||||
    public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
 | 
			
		||||
        => !(left == right);
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
        => obj is TypedKey<TData> o && o == this;
 | 
			
		||||
 | 
			
		||||
    public override int GetHashCode() => Key?.GetHashCode() ?? 0;
 | 
			
		||||
    public override int GetHashCode()
 | 
			
		||||
        => Key?.GetHashCode() ?? 0;
 | 
			
		||||
 | 
			
		||||
    public override string ToString() => Key;
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
        => Key;
 | 
			
		||||
}
 | 
			
		||||
@@ -10,28 +10,31 @@ public class YamlSeria : IConfigSeria
 | 
			
		||||
    private readonly ISerializer _serializer;
 | 
			
		||||
    private readonly IDeserializer _deserializer;
 | 
			
		||||
 | 
			
		||||
    private static readonly Regex CodePointRegex
 | 
			
		||||
        = new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
 | 
			
		||||
            RegexOptions.Compiled);
 | 
			
		||||
    private static readonly Regex _codePointRegex =
 | 
			
		||||
        new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
 | 
			
		||||
            RegexOptions.Compiled
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    public YamlSeria()
 | 
			
		||||
    {
 | 
			
		||||
        _serializer = Yaml.Serializer;
 | 
			
		||||
        _deserializer = Yaml.Deserializer;
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public string Serialize<T>(T obj)
 | 
			
		||||
    {
 | 
			
		||||
        var escapedOutput = _serializer.Serialize(obj);
 | 
			
		||||
        var output = CodePointRegex.Replace(escapedOutput, me =>
 | 
			
		||||
        {
 | 
			
		||||
            var str = me.Groups["code"].Value;
 | 
			
		||||
            var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
 | 
			
		||||
            return newString;
 | 
			
		||||
        });
 | 
			
		||||
        var output = _codePointRegex.Replace(escapedOutput,
 | 
			
		||||
            me =>
 | 
			
		||||
            {
 | 
			
		||||
                var str = me.Groups["code"].Value;
 | 
			
		||||
                var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
 | 
			
		||||
                return newString;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        return output;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public T Deserialize<T>(string data) 
 | 
			
		||||
    public T Deserialize<T>(string data)
 | 
			
		||||
        => _deserializer.Deserialize<T>(data);
 | 
			
		||||
}
 | 
			
		||||
@@ -5,21 +5,29 @@ namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class ReplacementBuilder
 | 
			
		||||
{
 | 
			
		||||
    private static readonly Regex rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
 | 
			
		||||
    private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
 | 
			
		||||
        RegexOptions.Compiled
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
 | 
			
		||||
 | 
			
		||||
    public ReplacementBuilder()
 | 
			
		||||
        => WithRngRegex();
 | 
			
		||||
 | 
			
		||||
    public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, SocketGuild g, DiscordSocketClient client)
 | 
			
		||||
        => this.WithUser(usr)
 | 
			
		||||
            .WithChannel(ch)
 | 
			
		||||
            .WithServer(client, g)
 | 
			
		||||
            .WithClient(client);
 | 
			
		||||
    public ReplacementBuilder WithDefault(
 | 
			
		||||
        IUser usr,
 | 
			
		||||
        IMessageChannel ch,
 | 
			
		||||
        SocketGuild g,
 | 
			
		||||
        DiscordSocketClient client)
 | 
			
		||||
        => this.WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client);
 | 
			
		||||
 | 
			
		||||
    public ReplacementBuilder WithDefault(ICommandContext ctx) =>
 | 
			
		||||
        WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
 | 
			
		||||
    public ReplacementBuilder WithDefault(ICommandContext ctx)
 | 
			
		||||
        => WithDefault(ctx.User,
 | 
			
		||||
            ctx.Channel,
 | 
			
		||||
            ctx.Guild as SocketGuild,
 | 
			
		||||
            (DiscordSocketClient)ctx.Client
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    public ReplacementBuilder WithMention(DiscordSocketClient client)
 | 
			
		||||
    {
 | 
			
		||||
@@ -30,12 +38,14 @@ public class ReplacementBuilder
 | 
			
		||||
    public ReplacementBuilder WithClient(DiscordSocketClient client)
 | 
			
		||||
    {
 | 
			
		||||
        WithMention(client);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        _reps.TryAdd("%bot.status%", () => client.Status.ToString());
 | 
			
		||||
        _reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
 | 
			
		||||
        _reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
 | 
			
		||||
        _reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
 | 
			
		||||
        _reps.TryAdd("%bot.time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
 | 
			
		||||
        _reps.TryAdd("%bot.time%",
 | 
			
		||||
            () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())
 | 
			
		||||
        );
 | 
			
		||||
        _reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
 | 
			
		||||
        _reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
 | 
			
		||||
        _reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl()?.ToString());
 | 
			
		||||
@@ -51,19 +61,20 @@ public class ReplacementBuilder
 | 
			
		||||
        _reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
 | 
			
		||||
        _reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
 | 
			
		||||
        _reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
 | 
			
		||||
        _reps.TryAdd("%server.time%", () =>
 | 
			
		||||
        {
 | 
			
		||||
            var to = TimeZoneInfo.Local;
 | 
			
		||||
            if (g != null)
 | 
			
		||||
        _reps.TryAdd("%server.time%",
 | 
			
		||||
            () =>
 | 
			
		||||
            {
 | 
			
		||||
                if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
 | 
			
		||||
                    to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
 | 
			
		||||
            }
 | 
			
		||||
                var to = TimeZoneInfo.Local;
 | 
			
		||||
                if (g != 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();
 | 
			
		||||
        });
 | 
			
		||||
                return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ") +
 | 
			
		||||
                       to.StandardName.GetInitials();
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +91,7 @@ public class ReplacementBuilder
 | 
			
		||||
 | 
			
		||||
    public ReplacementBuilder WithUser(IUser user)
 | 
			
		||||
    {
 | 
			
		||||
        WithManyUsers(new[] {user});
 | 
			
		||||
        WithManyUsers(new[] { user });
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -92,10 +103,18 @@ public class ReplacementBuilder
 | 
			
		||||
        _reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
 | 
			
		||||
        _reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString())));
 | 
			
		||||
        _reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
 | 
			
		||||
        _reps.TryAdd("%user.created_time%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
 | 
			
		||||
        _reps.TryAdd("%user.created_date%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
 | 
			
		||||
        _reps.TryAdd("%user.joined_time%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
 | 
			
		||||
        _reps.TryAdd("%user.joined_date%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
 | 
			
		||||
        _reps.TryAdd("%user.created_time%",
 | 
			
		||||
            () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm")))
 | 
			
		||||
        );
 | 
			
		||||
        _reps.TryAdd("%user.created_date%",
 | 
			
		||||
            () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy")))
 | 
			
		||||
        );
 | 
			
		||||
        _reps.TryAdd("%user.joined_time%",
 | 
			
		||||
            () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"))
 | 
			
		||||
        );
 | 
			
		||||
        _reps.TryAdd("%user.joined_date%",
 | 
			
		||||
            () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"))
 | 
			
		||||
        );
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -110,21 +129,24 @@ public class ReplacementBuilder
 | 
			
		||||
    public ReplacementBuilder WithRngRegex()
 | 
			
		||||
    {
 | 
			
		||||
        var rng = new NadekoRandom();
 | 
			
		||||
        _regex.TryAdd(rngRegex, match =>
 | 
			
		||||
        {
 | 
			
		||||
            if (!int.TryParse(match.Groups["from"].ToString(), out var from))
 | 
			
		||||
                from = 0;
 | 
			
		||||
            if (!int.TryParse(match.Groups["to"].ToString(), out var to))
 | 
			
		||||
                to = 0;
 | 
			
		||||
        _regex.TryAdd(_rngRegex,
 | 
			
		||||
            match =>
 | 
			
		||||
            {
 | 
			
		||||
                if (!int.TryParse(match.Groups["from"].ToString(), out var from))
 | 
			
		||||
                    from = 0;
 | 
			
		||||
                if (!int.TryParse(match.Groups["to"].ToString(), out var to))
 | 
			
		||||
                    to = 0;
 | 
			
		||||
 | 
			
		||||
            if (from == 0 && to == 0)
 | 
			
		||||
                return rng.Next(0, 11).ToString();
 | 
			
		||||
                if (from == 0 &&
 | 
			
		||||
                    to == 0)
 | 
			
		||||
                    return rng.Next(0, 11).ToString();
 | 
			
		||||
 | 
			
		||||
            if (from >= to)
 | 
			
		||||
                return string.Empty;
 | 
			
		||||
                if (from >= to)
 | 
			
		||||
                    return string.Empty;
 | 
			
		||||
 | 
			
		||||
            return rng.Next(from, to + 1).ToString();
 | 
			
		||||
        });
 | 
			
		||||
                return rng.Next(from, to + 1).ToString();
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,10 @@ public class Replacer
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
            return input;
 | 
			
		||||
 | 
			
		||||
        foreach (var (Key, Text) in _replacements)
 | 
			
		||||
        foreach (var (key, text) in _replacements)
 | 
			
		||||
        {
 | 
			
		||||
            if (input.Contains(Key))
 | 
			
		||||
                input = input.Replace(Key, Text(), StringComparison.InvariantCulture);
 | 
			
		||||
            if (input.Contains(key))
 | 
			
		||||
                input = input.Replace(key, text(), StringComparison.InvariantCulture);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var item in _regex)
 | 
			
		||||
@@ -58,8 +58,7 @@ public class Replacer
 | 
			
		||||
        {
 | 
			
		||||
            newEmbedData.Author = new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = Replace(embedData.Author.Name),
 | 
			
		||||
                IconUrl = Replace(embedData.Author.IconUrl)
 | 
			
		||||
                Name = Replace(embedData.Author.Name), IconUrl = Replace(embedData.Author.IconUrl)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -70,9 +69,7 @@ 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);
 | 
			
		||||
            }
 | 
			
		||||
@@ -84,8 +81,7 @@ public class Replacer
 | 
			
		||||
        {
 | 
			
		||||
            newEmbedData.Footer = new()
 | 
			
		||||
            {
 | 
			
		||||
                Text = Replace(embedData.Footer.Text),
 | 
			
		||||
                IconUrl = Replace(embedData.Footer.IconUrl)
 | 
			
		||||
                Text = Replace(embedData.Footer.Text), IconUrl = Replace(embedData.Footer.IconUrl)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,7 @@ public struct ShmartNumber : IEquatable<ShmartNumber>
 | 
			
		||||
        => Value.ToString();
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
        => obj is ShmartNumber sn
 | 
			
		||||
            ? Equals(sn)
 | 
			
		||||
            : false;
 | 
			
		||||
        => obj is ShmartNumber sn && Equals(sn);
 | 
			
		||||
 | 
			
		||||
    public bool Equals(ShmartNumber other)
 | 
			
		||||
        => other.Value == Value;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,14 +15,13 @@ public sealed record SmartEmbedText : SmartText
 | 
			
		||||
 | 
			
		||||
    public uint Color { get; set; } = 7458112;
 | 
			
		||||
 | 
			
		||||
    public bool IsValid =>
 | 
			
		||||
        !string.IsNullOrWhiteSpace(Title) ||
 | 
			
		||||
        !string.IsNullOrWhiteSpace(Description) ||
 | 
			
		||||
        !string.IsNullOrWhiteSpace(Url) ||
 | 
			
		||||
        !string.IsNullOrWhiteSpace(Thumbnail) ||
 | 
			
		||||
        !string.IsNullOrWhiteSpace(Image) ||
 | 
			
		||||
        (Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) ||
 | 
			
		||||
        Fields is { Length: > 0 };
 | 
			
		||||
    public bool IsValid
 | 
			
		||||
        => !string.IsNullOrWhiteSpace(Title) || !string.IsNullOrWhiteSpace(Description) ||
 | 
			
		||||
           !string.IsNullOrWhiteSpace(Url) || !string.IsNullOrWhiteSpace(Thumbnail) ||
 | 
			
		||||
           !string.IsNullOrWhiteSpace(Image) ||
 | 
			
		||||
           (Footer != null &&
 | 
			
		||||
            (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) ||
 | 
			
		||||
           Fields is { Length: > 0 };
 | 
			
		||||
 | 
			
		||||
    public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
 | 
			
		||||
    {
 | 
			
		||||
@@ -34,42 +33,23 @@ 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,
 | 
			
		||||
                })
 | 
			
		||||
            set.Fields = eb.Fields.Select(field
 | 
			
		||||
                    => new SmartTextEmbedField() { Inline = field.Inline, Name = field.Name, Value = field.Value, }
 | 
			
		||||
                )
 | 
			
		||||
                .ToArray();
 | 
			
		||||
 | 
			
		||||
        set.Color = eb.Color?.RawValue ?? 0;
 | 
			
		||||
        return set;
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public EmbedBuilder GetEmbed()
 | 
			
		||||
    {
 | 
			
		||||
        var embed = new EmbedBuilder()
 | 
			
		||||
            .WithColor(Color);
 | 
			
		||||
        var embed = new EmbedBuilder().WithColor(Color);
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(Title))
 | 
			
		||||
            embed.WithTitle(Title);
 | 
			
		||||
@@ -77,26 +57,31 @@ public sealed record SmartEmbedText : SmartText
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(Description))
 | 
			
		||||
            embed.WithDescription(Description);
 | 
			
		||||
 | 
			
		||||
        if (Url != null && Uri.IsWellFormedUriString(Url, UriKind.Absolute))
 | 
			
		||||
        if (Url != null &&
 | 
			
		||||
            Uri.IsWellFormedUriString(Url, UriKind.Absolute))
 | 
			
		||||
            embed.WithUrl(Url);
 | 
			
		||||
 | 
			
		||||
        if (Footer != null)
 | 
			
		||||
        {
 | 
			
		||||
            embed.WithFooter(efb =>
 | 
			
		||||
            {
 | 
			
		||||
                efb.WithText(Footer.Text);
 | 
			
		||||
                if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute))
 | 
			
		||||
                    efb.WithIconUrl(Footer.IconUrl);
 | 
			
		||||
            });
 | 
			
		||||
                {
 | 
			
		||||
                    efb.WithText(Footer.Text);
 | 
			
		||||
                    if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute))
 | 
			
		||||
                        efb.WithIconUrl(Footer.IconUrl);
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute))
 | 
			
		||||
        if (Thumbnail != null &&
 | 
			
		||||
            Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute))
 | 
			
		||||
            embed.WithThumbnailUrl(Thumbnail);
 | 
			
		||||
 | 
			
		||||
        if (Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute))
 | 
			
		||||
        if (Image != null &&
 | 
			
		||||
            Uri.IsWellFormedUriString(Image, UriKind.Absolute))
 | 
			
		||||
            embed.WithImageUrl(Image);
 | 
			
		||||
 | 
			
		||||
        if (Author != null && !string.IsNullOrWhiteSpace(Author.Name))
 | 
			
		||||
        if (Author != null &&
 | 
			
		||||
            !string.IsNullOrWhiteSpace(Author.Name))
 | 
			
		||||
        {
 | 
			
		||||
            if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute))
 | 
			
		||||
                Author.IconUrl = null;
 | 
			
		||||
@@ -110,7 +95,8 @@ public sealed record SmartEmbedText : SmartText
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var f in Fields)
 | 
			
		||||
            {
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value))
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(f.Name) &&
 | 
			
		||||
                    !string.IsNullOrWhiteSpace(f.Value))
 | 
			
		||||
                    embed.AddField(f.Name, f.Value, f.Inline);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,11 @@ namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public abstract record SmartText
 | 
			
		||||
{
 | 
			
		||||
    public bool IsEmbed => this is SmartEmbedText;
 | 
			
		||||
    public bool IsPlainText => this is SmartPlainText;
 | 
			
		||||
    public bool IsEmbed
 | 
			
		||||
        => this is SmartEmbedText;
 | 
			
		||||
 | 
			
		||||
    public bool IsPlainText
 | 
			
		||||
        => this is SmartPlainText;
 | 
			
		||||
 | 
			
		||||
    public static SmartText operator +(SmartText text, string input)
 | 
			
		||||
        => text switch
 | 
			
		||||
@@ -14,7 +17,7 @@ public abstract record SmartText
 | 
			
		||||
            SmartPlainText spt => new SmartPlainText(spt.Text + input),
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static SmartText operator +(string input, SmartText text)
 | 
			
		||||
        => text switch
 | 
			
		||||
        {
 | 
			
		||||
@@ -22,10 +25,11 @@ public abstract record SmartText
 | 
			
		||||
            SmartPlainText spt => new SmartPlainText(input + spt.Text),
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static SmartText CreateFrom(string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{"))
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input) ||
 | 
			
		||||
            !input.TrimStart().StartsWith("{"))
 | 
			
		||||
        {
 | 
			
		||||
            return new SmartPlainText(input);
 | 
			
		||||
        }
 | 
			
		||||
@@ -34,6 +38,9 @@ public abstract record SmartText
 | 
			
		||||
        {
 | 
			
		||||
            var smartEmbedText = JsonConvert.DeserializeObject<SmartEmbedText>(input);
 | 
			
		||||
 | 
			
		||||
            if (smartEmbedText is null)
 | 
			
		||||
                throw new();
 | 
			
		||||
            
 | 
			
		||||
            smartEmbedText.NormalizeFields();
 | 
			
		||||
 | 
			
		||||
            if (!smartEmbedText.IsValid)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@ namespace NadekoBot;
 | 
			
		||||
public class SmartTextEmbedAuthor
 | 
			
		||||
{
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
    public string IconUrl { get; set; }
 | 
			
		||||
    [JsonProperty("icon_url")]
 | 
			
		||||
    private string Icon_Url { set => IconUrl = value; }
 | 
			
		||||
    public string IconUrl { get; set; }
 | 
			
		||||
    public string Url { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,11 @@
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
// todo test smarttextembedfooter and smarttextembedauthor
 | 
			
		||||
 | 
			
		||||
public class SmartTextEmbedFooter
 | 
			
		||||
{
 | 
			
		||||
    public string Text { get; set; }
 | 
			
		||||
    public string IconUrl { get; set; }
 | 
			
		||||
    [JsonProperty("icon_url")]
 | 
			
		||||
    private string Icon_Url { set => IconUrl = value; }
 | 
			
		||||
    public string IconUrl { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,36 +17,37 @@ public sealed class ReactionEventWrapper : IDisposable
 | 
			
		||||
        _client.ReactionsCleared += Discord_ReactionsCleared;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Discord_ReactionsCleared(
 | 
			
		||||
        Cacheable<IUserMessage, ulong> msg,
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> channel)
 | 
			
		||||
    private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, Cacheable<IMessageChannel, ulong> channel)
 | 
			
		||||
    {
 | 
			
		||||
        Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (msg.Id == Message.Id)
 | 
			
		||||
                    OnReactionsCleared?.Invoke();
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (msg.Id == Message.Id)
 | 
			
		||||
                        OnReactionsCleared?.Invoke();
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        });
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Discord_ReactionRemoved(
 | 
			
		||||
        Cacheable<IUserMessage, ulong> msg,
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> cacheable, SocketReaction reaction)
 | 
			
		||||
        Cacheable<IMessageChannel, ulong> cacheable,
 | 
			
		||||
        SocketReaction reaction)
 | 
			
		||||
    {
 | 
			
		||||
        Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (msg.Id == Message.Id)
 | 
			
		||||
                    OnReactionRemoved?.Invoke(reaction);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (msg.Id == Message.Id)
 | 
			
		||||
                        OnReactionRemoved?.Invoke(reaction);
 | 
			
		||||
                }
 | 
			
		||||
                catch { }
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        });
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
@@ -57,16 +58,17 @@ public sealed class ReactionEventWrapper : IDisposable
 | 
			
		||||
        SocketReaction reaction)
 | 
			
		||||
    {
 | 
			
		||||
        Task.Run(() =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (msg.Id == Message.Id)
 | 
			
		||||
                    OnReactionAdded?.Invoke(reaction);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (msg.Id == Message.Id)
 | 
			
		||||
                        OnReactionAdded?.Invoke(reaction);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -36,10 +36,7 @@ public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
 | 
			
		||||
    private readonly CustomReactionsService _crs;
 | 
			
		||||
    private readonly CommandHandler _commandHandler;
 | 
			
		||||
 | 
			
		||||
    public CommandOrCrTypeReader(
 | 
			
		||||
        CommandService cmds,
 | 
			
		||||
        CustomReactionsService crs,
 | 
			
		||||
        CommandHandler commandHandler)
 | 
			
		||||
    public CommandOrCrTypeReader(CommandService cmds, CustomReactionsService crs, CommandHandler commandHandler)
 | 
			
		||||
    {
 | 
			
		||||
        _cmds = cmds;
 | 
			
		||||
        _crs = crs;
 | 
			
		||||
@@ -58,8 +55,12 @@ public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
 | 
			
		||||
        var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(context, input).ConfigureAwait(false);
 | 
			
		||||
        if (cmd.IsSuccess)
 | 
			
		||||
        {
 | 
			
		||||
            return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name, CommandOrCrInfo.Type.Normal));
 | 
			
		||||
            return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name,
 | 
			
		||||
                    CommandOrCrInfo.Type.Normal
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or cr found.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -74,7 +75,9 @@ public class CommandOrCrInfo
 | 
			
		||||
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
    public Type CmdType { get; set; }
 | 
			
		||||
    public bool IsCustom => CmdType == Type.Custom;
 | 
			
		||||
 | 
			
		||||
    public bool IsCustom
 | 
			
		||||
        => CmdType == Type.Custom;
 | 
			
		||||
 | 
			
		||||
    public CommandOrCrInfo(string input, Type type)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,11 @@ public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
 | 
			
		||||
    public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
 | 
			
		||||
    {
 | 
			
		||||
        var gdt = Parse(context.Guild.Id, input);
 | 
			
		||||
        if(gdt is null)
 | 
			
		||||
            return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format."));
 | 
			
		||||
        if (gdt is null)
 | 
			
		||||
            return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed,
 | 
			
		||||
                    "Input string is in an incorrect format."
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult(TypeReaderResult.FromSuccess(gdt));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,9 @@ public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
 | 
			
		||||
    public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
 | 
			
		||||
    {
 | 
			
		||||
        input = input.ToUpperInvariant();
 | 
			
		||||
        var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key;
 | 
			
		||||
        var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
 | 
			
		||||
            .FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
 | 
			
		||||
            ?.Key;
 | 
			
		||||
        if (module is null)
 | 
			
		||||
            return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found."));
 | 
			
		||||
 | 
			
		||||
@@ -28,14 +30,14 @@ public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
 | 
			
		||||
    public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
 | 
			
		||||
    {
 | 
			
		||||
        input = input.ToUpperInvariant();
 | 
			
		||||
        var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key;
 | 
			
		||||
        if (module is null && input != "ACTUALCUSTOMREACTIONS")
 | 
			
		||||
        var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
 | 
			
		||||
            .FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
 | 
			
		||||
            ?.Key;
 | 
			
		||||
        if (module is null &&
 | 
			
		||||
            input != "ACTUALCUSTOMREACTIONS")
 | 
			
		||||
            return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found."));
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
 | 
			
		||||
        {
 | 
			
		||||
            Name = input,
 | 
			
		||||
        }));
 | 
			
		||||
        return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo { Name = input, }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors )]
 | 
			
		||||
public abstract class NadekoTypeReader<T> : TypeReader
 | 
			
		||||
{
 | 
			
		||||
    public abstract Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input);
 | 
			
		||||
 
 | 
			
		||||
@@ -63,12 +63,10 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
 | 
			
		||||
            case "MAX":
 | 
			
		||||
                args.Result = Max(ctx);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly Regex percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
 | 
			
		||||
    private static readonly Regex _percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    private long Cur(ICommandContext ctx)
 | 
			
		||||
    {
 | 
			
		||||
@@ -80,15 +78,13 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
 | 
			
		||||
    {
 | 
			
		||||
        var settings = _gambling.Data;
 | 
			
		||||
        var max = settings.MaxBet;
 | 
			
		||||
        return max == 0
 | 
			
		||||
            ? Cur(ctx)
 | 
			
		||||
            : max;
 | 
			
		||||
        return max == 0 ? Cur(ctx) : max;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool TryHandlePercentage(ICommandContext ctx, string input, out long num)
 | 
			
		||||
    {
 | 
			
		||||
        num = 0;
 | 
			
		||||
        var m = percentRegex.Match(input);
 | 
			
		||||
        var m = _percentRegex.Match(input);
 | 
			
		||||
        if (m.Captures.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
 | 
			
		||||
@@ -97,6 +93,7 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
 | 
			
		||||
            num = (long)(Cur(ctx) * (percent / 100.0f));
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,9 +12,7 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
 | 
			
		||||
        => this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException("innerTypeDescriptor");
 | 
			
		||||
 | 
			
		||||
    public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
 | 
			
		||||
        => innerTypeDescriptor
 | 
			
		||||
            .GetProperties(type, container)
 | 
			
		||||
            .Select(d => new CommentsPropertyDescriptor(d));
 | 
			
		||||
        => innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d));
 | 
			
		||||
 | 
			
		||||
    private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
 | 
			
		||||
    {
 | 
			
		||||
@@ -31,14 +29,16 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
 | 
			
		||||
        public Type Type
 | 
			
		||||
            => baseDescriptor.Type;
 | 
			
		||||
 | 
			
		||||
        public Type TypeOverride {
 | 
			
		||||
        public Type TypeOverride
 | 
			
		||||
        {
 | 
			
		||||
            get => baseDescriptor.TypeOverride;
 | 
			
		||||
            set => baseDescriptor.TypeOverride = value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public int Order { get; set; }
 | 
			
		||||
 | 
			
		||||
        public ScalarStyle ScalarStyle {
 | 
			
		||||
        public ScalarStyle ScalarStyle
 | 
			
		||||
        {
 | 
			
		||||
            get => baseDescriptor.ScalarStyle;
 | 
			
		||||
            set => baseDescriptor.ScalarStyle = value;
 | 
			
		||||
        }
 | 
			
		||||
@@ -49,7 +49,8 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
 | 
			
		||||
        public void Write(object target, object value)
 | 
			
		||||
            => baseDescriptor.Write(target, value);
 | 
			
		||||
 | 
			
		||||
        public T GetCustomAttribute<T>() where T : Attribute
 | 
			
		||||
        public T GetCustomAttribute<T>()
 | 
			
		||||
            where T : Attribute
 | 
			
		||||
            => baseDescriptor.GetCustomAttribute<T>();
 | 
			
		||||
 | 
			
		||||
        public IObjectDescriptor Read(object target)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
 | 
			
		||||
 | 
			
		||||
    public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
 | 
			
		||||
    {
 | 
			
		||||
        var commentsDescriptor = value as CommentsObjectDescriptor;
 | 
			
		||||
        if (commentsDescriptor != null && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
 | 
			
		||||
        if (value is CommentsObjectDescriptor commentsDescriptor &&
 | 
			
		||||
            !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
 | 
			
		||||
        {
 | 
			
		||||
            context.Emit(new Comment(commentsDescriptor.Comment.Replace("\n", "\n# "), false));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,12 @@ namespace NadekoBot.Common.Yml;
 | 
			
		||||
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
 | 
			
		||||
{
 | 
			
		||||
    public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
 | 
			
		||||
        : base(nextEmitter) { }
 | 
			
		||||
        : base(nextEmitter)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
 | 
			
		||||
        {
 | 
			
		||||
            var value = eventInfo.Source.Value as string;
 | 
			
		||||
@@ -19,10 +20,7 @@ public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
 | 
			
		||||
            {
 | 
			
		||||
                var isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
 | 
			
		||||
                if (isMultiLine)
 | 
			
		||||
                    eventInfo = new(eventInfo.Source)
 | 
			
		||||
                    {
 | 
			
		||||
                        Style = ScalarStyle.Literal,
 | 
			
		||||
                    };
 | 
			
		||||
                    eventInfo = new(eventInfo.Source) { Style = ScalarStyle.Literal, };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@ public class Rgba32Converter : IYamlTypeConverter
 | 
			
		||||
    public void WriteYaml(IEmitter emitter, object value, Type type)
 | 
			
		||||
    {
 | 
			
		||||
        var color = (Rgba32)value;
 | 
			
		||||
        var val = (uint) ((color.B << 0) | (color.G << 8) | (color.R << 16));
 | 
			
		||||
        var val = (uint)((color.B << 0) | (color.G << 8) | (color.R << 16));
 | 
			
		||||
        emitter.Emit(new Scalar(val.ToString("X6").ToLower()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
public class CultureInfoConverter : IYamlTypeConverter
 | 
			
		||||
{
 | 
			
		||||
    public bool Accepts(Type type)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,22 +4,23 @@ namespace NadekoBot.Common.Yml;
 | 
			
		||||
 | 
			
		||||
public class Yaml
 | 
			
		||||
{
 | 
			
		||||
    public static ISerializer Serializer => new SerializerBuilder()
 | 
			
		||||
        .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
 | 
			
		||||
        .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
 | 
			
		||||
        .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
 | 
			
		||||
        .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
 | 
			
		||||
        .WithIndentedSequences()
 | 
			
		||||
        .WithTypeConverter(new Rgba32Converter())
 | 
			
		||||
        .WithTypeConverter(new CultureInfoConverter())
 | 
			
		||||
        .WithTypeConverter(new UriConverter())
 | 
			
		||||
        .Build();
 | 
			
		||||
    public static ISerializer Serializer
 | 
			
		||||
        => new SerializerBuilder().WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
 | 
			
		||||
            .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
 | 
			
		||||
            .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
 | 
			
		||||
            .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
 | 
			
		||||
            .WithIndentedSequences()
 | 
			
		||||
            .WithTypeConverter(new Rgba32Converter())
 | 
			
		||||
            .WithTypeConverter(new CultureInfoConverter())
 | 
			
		||||
            .WithTypeConverter(new UriConverter())
 | 
			
		||||
            .Build();
 | 
			
		||||
 | 
			
		||||
    public static IDeserializer Deserializer => new DeserializerBuilder()
 | 
			
		||||
        .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
 | 
			
		||||
        .WithTypeConverter(new Rgba32Converter())
 | 
			
		||||
        .WithTypeConverter(new CultureInfoConverter())
 | 
			
		||||
        .WithTypeConverter(new UriConverter())
 | 
			
		||||
        .IgnoreUnmatchedProperties()
 | 
			
		||||
        .Build();
 | 
			
		||||
    public static IDeserializer Deserializer
 | 
			
		||||
        => new DeserializerBuilder()
 | 
			
		||||
            .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
 | 
			
		||||
            .WithTypeConverter(new Rgba32Converter())
 | 
			
		||||
            .WithTypeConverter(new CultureInfoConverter())
 | 
			
		||||
            .WithTypeConverter(new UriConverter())
 | 
			
		||||
            .IgnoreUnmatchedProperties()
 | 
			
		||||
            .Build();
 | 
			
		||||
}
 | 
			
		||||
@@ -15,27 +15,28 @@ public class YamlHelper
 | 
			
		||||
 | 
			
		||||
        // Scan the character value.
 | 
			
		||||
 | 
			
		||||
        foreach(var c in point)
 | 
			
		||||
        foreach (var c in 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)
 | 
			
		||||
        if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
 | 
			
		||||
        {
 | 
			
		||||
            return point;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return char.ConvertFromUtf32(character);
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static bool IsHex(char c)
 | 
			
		||||
        => c is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f';
 | 
			
		||||
        => c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f');
 | 
			
		||||
 | 
			
		||||
    public static int AsHex(char c)
 | 
			
		||||
    {
 | 
			
		||||
@@ -43,10 +44,12 @@ public class YamlHelper
 | 
			
		||||
        {
 | 
			
		||||
            return c - '0';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (c <= 'F')
 | 
			
		||||
        {
 | 
			
		||||
            return c - 'A' + 10;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return c - 'a' + 10;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,12 +13,13 @@ public static class ClubExtensions
 | 
			
		||||
            .ThenInclude(x => x.User)
 | 
			
		||||
            .Include(x => x.Users)
 | 
			
		||||
            .AsQueryable();
 | 
			
		||||
 | 
			
		||||
    public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
 | 
			
		||||
        => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId)
 | 
			
		||||
        => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId
 | 
			
		||||
                                              || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin));
 | 
			
		||||
        => Include(clubs)
 | 
			
		||||
            .FirstOrDefault(c => c.Owner.UserId == userId || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin));
 | 
			
		||||
 | 
			
		||||
    public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId)
 | 
			
		||||
        => Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId));
 | 
			
		||||
@@ -27,17 +28,9 @@ public static class ClubExtensions
 | 
			
		||||
        => Include(clubs).FirstOrDefault(c => c.Name.ToUpper() == name.ToUpper() && c.Discrim == discrim);
 | 
			
		||||
 | 
			
		||||
    public static int GetNextDiscrim(this DbSet<ClubInfo> clubs, string name)
 | 
			
		||||
        => Include(clubs)
 | 
			
		||||
            .Where(x => x.Name.ToUpper() == name.ToUpper())
 | 
			
		||||
            .Select(x => x.Discrim)
 | 
			
		||||
            .DefaultIfEmpty()
 | 
			
		||||
            .Max() + 1;
 | 
			
		||||
        => Include(clubs).Where(x => x.Name.ToUpper() == name.ToUpper()).Select(x => x.Discrim).DefaultIfEmpty().Max() +
 | 
			
		||||
           1;
 | 
			
		||||
 | 
			
		||||
    public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page)
 | 
			
		||||
        => clubs
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .OrderByDescending(x => x.Xp)
 | 
			
		||||
            .Skip(page * 9)
 | 
			
		||||
            .Take(9)
 | 
			
		||||
            .ToList();
 | 
			
		||||
        => clubs.AsNoTracking().OrderByDescending(x => x.Xp).Skip(page * 9).Take(9).ToList();
 | 
			
		||||
}
 | 
			
		||||
@@ -10,11 +10,7 @@ public static class CustomReactionsExtensions
 | 
			
		||||
        => crs.Delete(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<CustomReaction> ForId(this DbSet<CustomReaction> crs, ulong id)
 | 
			
		||||
        => crs
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == id)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
        => crs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
 | 
			
		||||
 | 
			
		||||
    public static CustomReaction GetByGuildIdAndInput(this DbSet<CustomReaction> crs, ulong? guildId, string input)
 | 
			
		||||
        => crs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ namespace NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
public static class DbExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static T GetById<T>(this DbSet<T> set, int id) where T: DbEntity
 | 
			
		||||
    public static T GetById<T>(this DbSet<T> set, int id)
 | 
			
		||||
        where T : DbEntity
 | 
			
		||||
        => set.FirstOrDefault(x => x.Id == id);
 | 
			
		||||
}
 | 
			
		||||
@@ -8,10 +8,15 @@ namespace NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
public static class DiscordUserExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static void EnsureUserCreated(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId)
 | 
			
		||||
        => ctx.DiscordUser
 | 
			
		||||
            .ToLinqToDBTable()
 | 
			
		||||
            .InsertOrUpdate(() => new()
 | 
			
		||||
    public static void EnsureUserCreated(
 | 
			
		||||
        this NadekoContext ctx,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        string username,
 | 
			
		||||
        string discrim,
 | 
			
		||||
        string avatarId)
 | 
			
		||||
        => ctx.DiscordUser.ToLinqToDBTable()
 | 
			
		||||
            .InsertOrUpdate(
 | 
			
		||||
                () => new()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = userId,
 | 
			
		||||
                    Username = username,
 | 
			
		||||
@@ -20,36 +25,44 @@ 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(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId)
 | 
			
		||||
    public static DiscordUser GetOrCreateUser(
 | 
			
		||||
        this NadekoContext ctx,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        string username,
 | 
			
		||||
        string discrim,
 | 
			
		||||
        string avatarId)
 | 
			
		||||
    {
 | 
			
		||||
        ctx.EnsureUserCreated(userId, username, discrim, avatarId);
 | 
			
		||||
        return ctx.DiscordUser
 | 
			
		||||
            .Include(x => x.Club)
 | 
			
		||||
        ctx.EnsureUserCreated(userId,
 | 
			
		||||
            username,
 | 
			
		||||
            discrim,
 | 
			
		||||
            avatarId
 | 
			
		||||
        );
 | 
			
		||||
        return ctx.DiscordUser.Include(x => x.Club)
 | 
			
		||||
            .First(u => u.UserId == userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static DiscordUser GetOrCreateUser(this NadekoContext ctx, IUser original)
 | 
			
		||||
        => ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId);
 | 
			
		||||
        => ctx.GetOrCreateUser(original.Id,
 | 
			
		||||
            original.Username,
 | 
			
		||||
            original.Discriminator,
 | 
			
		||||
            original.AvatarId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
 | 
			
		||||
        => users.AsQueryable()
 | 
			
		||||
            .Where(x => x.TotalXp > users
 | 
			
		||||
                .AsQueryable()
 | 
			
		||||
                .Where(y => y.UserId == id)
 | 
			
		||||
                .Select(y => y.TotalXp)
 | 
			
		||||
                .FirstOrDefault())
 | 
			
		||||
            .Count() + 1;
 | 
			
		||||
               .Where(x => x.TotalXp >
 | 
			
		||||
                           users.AsQueryable()
 | 
			
		||||
                               .Where(y => y.UserId == id)
 | 
			
		||||
                               .Select(y => y.TotalXp)
 | 
			
		||||
                               .FirstOrDefault()
 | 
			
		||||
               )
 | 
			
		||||
               .Count() +
 | 
			
		||||
           1;
 | 
			
		||||
 | 
			
		||||
    public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page)
 | 
			
		||||
        => users.AsQueryable()
 | 
			
		||||
@@ -59,7 +72,11 @@ public static class DiscordUserExtensions
 | 
			
		||||
            .AsEnumerable()
 | 
			
		||||
            .ToArray();
 | 
			
		||||
 | 
			
		||||
    public static List<DiscordUser> GetTopRichest(this DbSet<DiscordUser> users, ulong botId, int count, int page = 0)
 | 
			
		||||
    public static List<DiscordUser> GetTopRichest(
 | 
			
		||||
        this DbSet<DiscordUser> users,
 | 
			
		||||
        ulong botId,
 | 
			
		||||
        int count,
 | 
			
		||||
        int page = 0)
 | 
			
		||||
        => users.AsQueryable()
 | 
			
		||||
            .Where(c => c.CurrencyAmount > 0 && botId != c.UserId)
 | 
			
		||||
            .OrderByDescending(c => c.CurrencyAmount)
 | 
			
		||||
@@ -67,40 +84,44 @@ public static class DiscordUserExtensions
 | 
			
		||||
            .Take(count)
 | 
			
		||||
            .ToList();
 | 
			
		||||
 | 
			
		||||
    public static List<DiscordUser> GetTopRichest(this DbSet<DiscordUser> users, ulong botId, int count)
 | 
			
		||||
        => users.AsQueryable()
 | 
			
		||||
            .Where(c => c.CurrencyAmount > 0 && botId != c.UserId)
 | 
			
		||||
            .OrderByDescending(c => c.CurrencyAmount)
 | 
			
		||||
            .Take(count)
 | 
			
		||||
            .ToList();
 | 
			
		||||
 | 
			
		||||
    public static long GetUserCurrency(this DbSet<DiscordUser> users, ulong userId) =>
 | 
			
		||||
        users.AsNoTracking()
 | 
			
		||||
            .FirstOrDefault(x => x.UserId == userId)
 | 
			
		||||
            ?.CurrencyAmount ?? 0;
 | 
			
		||||
    public static long GetUserCurrency(this DbSet<DiscordUser> users, ulong userId)
 | 
			
		||||
        => users.AsNoTracking()
 | 
			
		||||
               .FirstOrDefault(x => x.UserId == userId)
 | 
			
		||||
               ?.CurrencyAmount ??
 | 
			
		||||
           0;
 | 
			
		||||
 | 
			
		||||
    public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
 | 
			
		||||
    {
 | 
			
		||||
        var items = users.AsQueryable().Where(x => ids.Contains(x.UserId));
 | 
			
		||||
        var items = users.AsQueryable()
 | 
			
		||||
            .Where(x => ids.Contains(x.UserId));
 | 
			
		||||
        foreach (var item in items)
 | 
			
		||||
        {
 | 
			
		||||
            item.CurrencyAmount = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool TryUpdateCurrencyState(this NadekoContext ctx, ulong userId, string name, string discrim, string avatarId, long amount, bool allowNegative = false)
 | 
			
		||||
    public static bool TryUpdateCurrencyState(
 | 
			
		||||
        this NadekoContext ctx,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        string name,
 | 
			
		||||
        string discrim,
 | 
			
		||||
        string avatarId,
 | 
			
		||||
        long amount,
 | 
			
		||||
        bool allowNegative = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (amount == 0)
 | 
			
		||||
            return true;
 | 
			
		||||
 | 
			
		||||
        // if remove - try to remove if he has more or equal than the amount
 | 
			
		||||
        // and return number of rows > 0 (was there a change)
 | 
			
		||||
        if (amount < 0 && !allowNegative)
 | 
			
		||||
        if (amount < 0 &&
 | 
			
		||||
            !allowNegative)
 | 
			
		||||
        {
 | 
			
		||||
            var rows = ctx.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
UPDATE DiscordUser
 | 
			
		||||
SET CurrencyAmount=CurrencyAmount+{amount}
 | 
			
		||||
WHERE UserId={userId} AND CurrencyAmount>={-amount};");
 | 
			
		||||
WHERE UserId={userId} AND CurrencyAmount>={-amount};"
 | 
			
		||||
            );
 | 
			
		||||
            return rows > 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +131,8 @@ WHERE UserId={userId} AND CurrencyAmount>={-amount};");
 | 
			
		||||
            var rows = ctx.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
UPDATE DiscordUser
 | 
			
		||||
SET CurrencyAmount=CurrencyAmount+{amount}
 | 
			
		||||
WHERE UserId={userId};");
 | 
			
		||||
WHERE UserId={userId};"
 | 
			
		||||
            );
 | 
			
		||||
            return rows > 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -118,22 +140,22 @@ WHERE UserId={userId};");
 | 
			
		||||
        // if it exists, sum current amount with the new one, if it doesn't
 | 
			
		||||
        // he just has the new amount
 | 
			
		||||
        var updatedUserData = !string.IsNullOrWhiteSpace(name);
 | 
			
		||||
        name = name ?? "Unknown";
 | 
			
		||||
        discrim = discrim ?? "????";
 | 
			
		||||
        avatarId = avatarId ?? "";
 | 
			
		||||
        name ??= "Unknown";
 | 
			
		||||
        discrim ??= "????";
 | 
			
		||||
        avatarId ??= "";
 | 
			
		||||
 | 
			
		||||
        // just update the amount, there is no new user data
 | 
			
		||||
        if (!updatedUserData)
 | 
			
		||||
        {
 | 
			
		||||
            var rows = ctx.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
            ctx.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
UPDATE OR IGNORE DiscordUser
 | 
			
		||||
SET CurrencyAmount=CurrencyAmount+{amount}
 | 
			
		||||
WHERE UserId={userId};
 | 
			
		||||
 | 
			
		||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
 | 
			
		||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
 | 
			
		||||
");
 | 
			
		||||
                
 | 
			
		||||
"
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -147,14 +169,15 @@ WHERE UserId={userId};
 | 
			
		||||
 | 
			
		||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
 | 
			
		||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
 | 
			
		||||
");
 | 
			
		||||
"
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
 | 
			
		||||
        => users
 | 
			
		||||
            .Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
 | 
			
		||||
        => users.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
 | 
			
		||||
 | 
			
		||||
    public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
 | 
			
		||||
        => users.AsQueryable()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ public static class GuildConfigExtensions
 | 
			
		||||
        public ulong GuildId { get; set; }
 | 
			
		||||
        public ulong ChannelId { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets full stream role settings for the guild with the specified id.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -21,31 +21,27 @@ public static class GuildConfigExtensions
 | 
			
		||||
    /// <returns>Guild'p stream role settings</returns>
 | 
			
		||||
    public static StreamRoleSettings GetStreamRoleSettings(this NadekoContext ctx, ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var conf = ctx.GuildConfigsForId(guildId, set => set.Include(y => y.StreamRole)
 | 
			
		||||
            .Include(y => y.StreamRole.Whitelist)
 | 
			
		||||
            .Include(y => y.StreamRole.Blacklist));
 | 
			
		||||
        var conf = ctx.GuildConfigsForId(guildId,
 | 
			
		||||
            set => set.Include(y => y.StreamRole)
 | 
			
		||||
                .Include(y => y.StreamRole.Whitelist)
 | 
			
		||||
                .Include(y => y.StreamRole.Blacklist)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (conf.StreamRole is null)
 | 
			
		||||
            conf.StreamRole = new();
 | 
			
		||||
 | 
			
		||||
        return conf.StreamRole;
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
    private static List<WarningPunishment> DefaultWarnPunishments =>
 | 
			
		||||
        new() {
 | 
			
		||||
            new() {
 | 
			
		||||
                Count = 3,
 | 
			
		||||
                Punishment = PunishmentAction.Kick
 | 
			
		||||
            },
 | 
			
		||||
            new() {
 | 
			
		||||
                Count = 5,
 | 
			
		||||
                Punishment = PunishmentAction.Ban
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    private static List<WarningPunishment> DefaultWarnPunishments
 | 
			
		||||
        => new()
 | 
			
		||||
        {
 | 
			
		||||
            new() { Count = 3, Punishment = PunishmentAction.Kick },
 | 
			
		||||
            new() { Count = 5, Punishment = PunishmentAction.Ban }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs)
 | 
			
		||||
        => configs
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => configs.AsQueryable()
 | 
			
		||||
            .AsSplitQuery()
 | 
			
		||||
            .Include(gc => gc.CommandCooldowns)
 | 
			
		||||
            .Include(gc => gc.FollowedStreams)
 | 
			
		||||
@@ -56,9 +52,10 @@ public static class GuildConfigExtensions
 | 
			
		||||
            .Include(gc => gc.ReactionRoleMessages)
 | 
			
		||||
            .ThenInclude(x => x.ReactionRoles);
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<GuildConfig> GetAllGuildConfigs(this DbSet<GuildConfig> configs, List<ulong> availableGuilds)
 | 
			
		||||
        => configs
 | 
			
		||||
            .IncludeEverything()
 | 
			
		||||
    public static IEnumerable<GuildConfig> GetAllGuildConfigs(
 | 
			
		||||
        this DbSet<GuildConfig> configs,
 | 
			
		||||
        List<ulong> availableGuilds)
 | 
			
		||||
        => configs.IncludeEverything()
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .Where(x => availableGuilds.Contains(x.GuildId))
 | 
			
		||||
            .ToList();
 | 
			
		||||
@@ -70,15 +67,16 @@ public static class GuildConfigExtensions
 | 
			
		||||
    /// <param name="guildId">Id of the guide</param>
 | 
			
		||||
    /// <param name="includes">Use to manipulate the set however you want. Pass null to include everything</param>
 | 
			
		||||
    /// <returns>Config for the guild</returns>
 | 
			
		||||
    public static GuildConfig GuildConfigsForId(this NadekoContext ctx, ulong guildId, Func<DbSet<GuildConfig>, IQueryable<GuildConfig>> includes)
 | 
			
		||||
    public static GuildConfig GuildConfigsForId(
 | 
			
		||||
        this NadekoContext ctx,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        Func<DbSet<GuildConfig>, IQueryable<GuildConfig>> includes)
 | 
			
		||||
    {
 | 
			
		||||
        GuildConfig config;
 | 
			
		||||
 | 
			
		||||
        if (includes is null)
 | 
			
		||||
        {
 | 
			
		||||
            config = ctx
 | 
			
		||||
                .GuildConfigs
 | 
			
		||||
                .IncludeEverything()
 | 
			
		||||
            config = ctx.GuildConfigs.IncludeEverything()
 | 
			
		||||
                .FirstOrDefault(c => c.GuildId == guildId);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -90,12 +88,13 @@ public static class GuildConfigExtensions
 | 
			
		||||
        if (config is null)
 | 
			
		||||
        {
 | 
			
		||||
            ctx.GuildConfigs.Add(config = new()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Permissions = Permissionv2.GetDefaultPermlist,
 | 
			
		||||
                WarningsInitialized = true,
 | 
			
		||||
                WarnPunishments = DefaultWarnPunishments,
 | 
			
		||||
            });
 | 
			
		||||
                {
 | 
			
		||||
                    GuildId = guildId,
 | 
			
		||||
                    Permissions = Permissionv2.GetDefaultPermlist,
 | 
			
		||||
                    WarningsInitialized = true,
 | 
			
		||||
                    WarnPunishments = DefaultWarnPunishments,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            ctx.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -110,21 +109,17 @@ public static class GuildConfigExtensions
 | 
			
		||||
 | 
			
		||||
    public static LogSetting LogSettingsFor(this NadekoContext ctx, ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var logSetting = ctx.LogSettings
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        var logSetting = ctx.LogSettings.AsQueryable()
 | 
			
		||||
            .Include(x => x.LogIgnores)
 | 
			
		||||
            .Where(x => x.GuildId == guildId)
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (logSetting is null)
 | 
			
		||||
        {
 | 
			
		||||
            ctx.LogSettings.Add(logSetting = new ()
 | 
			
		||||
            {
 | 
			
		||||
                GuildId = guildId
 | 
			
		||||
            });
 | 
			
		||||
            ctx.LogSettings.Add(logSetting = new() { GuildId = guildId });
 | 
			
		||||
            ctx.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        return logSetting;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -139,23 +134,18 @@ public static class GuildConfigExtensions
 | 
			
		||||
 | 
			
		||||
    public static GuildConfig GcWithPermissionsv2For(this NadekoContext ctx, ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var config = ctx
 | 
			
		||||
            .GuildConfigs
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        var config = ctx.GuildConfigs.AsQueryable()
 | 
			
		||||
            .Where(gc => gc.GuildId == guildId)
 | 
			
		||||
            .Include(gc => gc.Permissions)
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        else if (config.Permissions is null ||
 | 
			
		||||
                 !config.Permissions.Any()) // if no perms, add default ones
 | 
			
		||||
        {
 | 
			
		||||
            config.Permissions = Permissionv2.GetDefaultPermlist;
 | 
			
		||||
            ctx.SaveChanges();
 | 
			
		||||
@@ -165,8 +155,7 @@ public static class GuildConfigExtensions
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs)
 | 
			
		||||
        => configs
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => configs.AsQueryable()
 | 
			
		||||
            .Include(x => x.FollowedStreams)
 | 
			
		||||
            .SelectMany(gc => gc.FollowedStreams)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
@@ -196,7 +185,8 @@ public static class GuildConfigExtensions
 | 
			
		||||
                .Include(x => x.XpSettings)
 | 
			
		||||
                .ThenInclude(x => x.CurrencyRewards)
 | 
			
		||||
                .Include(x => x.XpSettings)
 | 
			
		||||
                .ThenInclude(x => x.ExclusionList));
 | 
			
		||||
                .ThenInclude(x => x.ExclusionList)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (gc.XpSettings is null)
 | 
			
		||||
            gc.XpSettings = new XpSettings();
 | 
			
		||||
@@ -205,15 +195,10 @@ public static class GuildConfigExtensions
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<GeneratingChannel> GetGeneratingChannels(this DbSet<GuildConfig> configs)
 | 
			
		||||
        => configs
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => configs.AsQueryable()
 | 
			
		||||
            .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();
 | 
			
		||||
}
 | 
			
		||||
@@ -7,17 +7,12 @@ public static class MusicPlayerSettingsExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static async Task<MusicPlayerSettings> ForGuildAsync(this DbSet<MusicPlayerSettings> settings, ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        var toReturn = await settings
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        var toReturn = await settings.AsQueryable()
 | 
			
		||||
            .FirstOrDefaultAsync(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,16 +10,14 @@ public static class MusicPlaylistExtensions
 | 
			
		||||
        if (num < 1)
 | 
			
		||||
            throw new IndexOutOfRangeException();
 | 
			
		||||
 | 
			
		||||
        return playlists
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        return playlists.AsQueryable()
 | 
			
		||||
            .Skip((num - 1) * 20)
 | 
			
		||||
            .Take(20)
 | 
			
		||||
            .Include(pl => pl.Songs)
 | 
			
		||||
            .ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static MusicPlaylist GetWithSongs(this DbSet<MusicPlaylist> playlists, int id) => 
 | 
			
		||||
        playlists
 | 
			
		||||
            .Include(mpl => mpl.Songs)
 | 
			
		||||
    public static MusicPlaylist GetWithSongs(this DbSet<MusicPlaylist> playlists, int id)
 | 
			
		||||
        => playlists.Include(mpl => mpl.Songs)
 | 
			
		||||
            .FirstOrDefault(mpl => mpl.Id == id);
 | 
			
		||||
}
 | 
			
		||||
@@ -13,27 +13,25 @@ public static class PollExtensions
 | 
			
		||||
 | 
			
		||||
    public static void RemovePoll(this NadekoContext ctx, int id)
 | 
			
		||||
    {
 | 
			
		||||
        var p = ctx
 | 
			
		||||
            .Poll
 | 
			
		||||
            .Include(x => x.Answers)
 | 
			
		||||
        var p = ctx.Poll.Include(x => x.Answers)
 | 
			
		||||
            .Include(x => x.Votes)
 | 
			
		||||
            .FirstOrDefault(x => x.Id == id);
 | 
			
		||||
 | 
			
		||||
        if (p is null)
 | 
			
		||||
            return;
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        if (p.Votes != null)
 | 
			
		||||
        {
 | 
			
		||||
            ctx.RemoveRange(p.Votes);
 | 
			
		||||
            p.Votes.Clear();
 | 
			
		||||
        }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        if (p.Answers != null)
 | 
			
		||||
        {
 | 
			
		||||
            ctx.RemoveRange(p.Answers);
 | 
			
		||||
            p.Answers.Clear();
 | 
			
		||||
        }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        ctx.Poll.Remove(p);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,43 +6,58 @@ namespace NadekoBot.Db;
 | 
			
		||||
public static class QuoteExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IEnumerable<Quote> GetForGuild(this DbSet<Quote> quotes, ulong guildId)
 | 
			
		||||
        => quotes.AsQueryable().Where(x => x.GuildId == guildId);
 | 
			
		||||
        => quotes.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == guildId);
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<Quote> GetGroup(this DbSet<Quote> quotes, ulong guildId, int page, OrderType order)
 | 
			
		||||
    public static IEnumerable<Quote> GetGroup(
 | 
			
		||||
        this DbSet<Quote> quotes,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        int page,
 | 
			
		||||
        OrderType order)
 | 
			
		||||
    {
 | 
			
		||||
        var q = quotes.AsQueryable().Where(x => x.GuildId == guildId);
 | 
			
		||||
        var q = quotes.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == guildId);
 | 
			
		||||
        if (order == OrderType.Keyword)
 | 
			
		||||
            q = q.OrderBy(x => x.Keyword);
 | 
			
		||||
        else
 | 
			
		||||
            q = q.OrderBy(x => x.Id);
 | 
			
		||||
 | 
			
		||||
        return q.Skip(15 * page).Take(15).ToArray();
 | 
			
		||||
        return q.Skip(15 * page)
 | 
			
		||||
            .Take(15)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task<Quote> GetRandomQuoteByKeywordAsync(this DbSet<Quote> quotes, ulong guildId, string keyword)
 | 
			
		||||
    public static async Task<Quote> GetRandomQuoteByKeywordAsync(
 | 
			
		||||
        this DbSet<Quote> quotes,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        string keyword)
 | 
			
		||||
    {
 | 
			
		||||
        var rng = new NadekoRandom();
 | 
			
		||||
        return (await quotes.AsQueryable()
 | 
			
		||||
                .Where(q => q.GuildId == guildId && q.Keyword == keyword)
 | 
			
		||||
                .ToListAsync())
 | 
			
		||||
            .OrderBy(q => rng.Next())
 | 
			
		||||
                .ToListAsync()).OrderBy(q => rng.Next())
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task<Quote> SearchQuoteKeywordTextAsync(this DbSet<Quote> quotes, ulong guildId, string keyword, string text)
 | 
			
		||||
    public static async Task<Quote> SearchQuoteKeywordTextAsync(
 | 
			
		||||
        this DbSet<Quote> quotes,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        string keyword,
 | 
			
		||||
        string text)
 | 
			
		||||
    {
 | 
			
		||||
        var rngk = new NadekoRandom();
 | 
			
		||||
        return (await quotes.AsQueryable()
 | 
			
		||||
                .Where(q => q.GuildId == guildId
 | 
			
		||||
                            && q.Keyword == keyword
 | 
			
		||||
                            && EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
 | 
			
		||||
                .Where(q => q.GuildId == guildId &&
 | 
			
		||||
                            q.Keyword == keyword &&
 | 
			
		||||
                            EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
 | 
			
		||||
                    // && q.Text.Contains(text, StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                )
 | 
			
		||||
                .ToListAsync())
 | 
			
		||||
            .OrderBy(q => rngk.Next())
 | 
			
		||||
                .ToListAsync()).OrderBy(q => rngk.Next())
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void RemoveAllByKeyword(this DbSet<Quote> quotes, ulong guildId, string keyword)
 | 
			
		||||
        => quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword));
 | 
			
		||||
        => quotes.RemoveRange(quotes.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword)
 | 
			
		||||
        );
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,9 @@ namespace NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
public static class ReminderExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IEnumerable<Reminder> GetIncludedReminders(this DbSet<Reminder> reminders, IEnumerable<ulong> guildIds) 
 | 
			
		||||
    public static IEnumerable<Reminder> GetIncludedReminders(
 | 
			
		||||
        this DbSet<Reminder> reminders,
 | 
			
		||||
        IEnumerable<ulong> guildIds)
 | 
			
		||||
        => reminders.AsQueryable()
 | 
			
		||||
            .Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0)
 | 
			
		||||
            .ToList();
 | 
			
		||||
@@ -16,7 +18,7 @@ public static class ReminderExtensions
 | 
			
		||||
            .OrderBy(x => x.DateAdded)
 | 
			
		||||
            .Skip(page * 10)
 | 
			
		||||
            .Take(10);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<Reminder> RemindersForServer(this DbSet<Reminder> reminders, ulong serverId, int page)
 | 
			
		||||
        => reminders.AsQueryable()
 | 
			
		||||
            .Where(x => x.ServerId == serverId)
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@ public static class SelfAssignableRolesExtensions
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId) 
 | 
			
		||||
        =>  roles.AsQueryable()
 | 
			
		||||
    public static IEnumerable<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId)
 | 
			
		||||
        => roles.AsQueryable()
 | 
			
		||||
            .Where(s => s.GuildId == guildId)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
}
 | 
			
		||||
@@ -14,20 +14,17 @@ 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static List<UserXpStats> GetUsersFor(this DbSet<UserXpStats> xps, ulong guildId, int page)
 | 
			
		||||
        => xps
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => xps.AsQueryable()
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .Where(x => x.GuildId == guildId)
 | 
			
		||||
            .OrderByDescending(x => x.Xp + x.AwardedXp)
 | 
			
		||||
@@ -36,8 +33,7 @@ public static class UserXpExtensions
 | 
			
		||||
            .ToList();
 | 
			
		||||
 | 
			
		||||
    public static List<UserXpStats> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
 | 
			
		||||
        => xps
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => xps.AsQueryable()
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .Where(x => x.GuildId == guildId)
 | 
			
		||||
            .OrderByDescending(x => x.Xp + x.AwardedXp)
 | 
			
		||||
@@ -51,15 +47,17 @@ public static class UserXpExtensions
 | 
			
		||||
        //	FROM UserXpStats
 | 
			
		||||
        //	WHERE UserId = @p2 AND GuildId = @p1
 | 
			
		||||
        //	LIMIT 1));";
 | 
			
		||||
        => xps
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .Where(x => x.GuildId == guildId && x.Xp + x.AwardedXp >
 | 
			
		||||
                xps.AsQueryable()
 | 
			
		||||
                    .Where(y => y.UserId == userId && y.GuildId == guildId)
 | 
			
		||||
                    .Select(y => y.Xp + y.AwardedXp)
 | 
			
		||||
                    .FirstOrDefault())
 | 
			
		||||
            .Count() + 1;
 | 
			
		||||
        => xps.AsQueryable()
 | 
			
		||||
               .AsNoTracking()
 | 
			
		||||
               .Where(x => x.GuildId == guildId &&
 | 
			
		||||
                           x.Xp + x.AwardedXp >
 | 
			
		||||
                           xps.AsQueryable()
 | 
			
		||||
                               .Where(y => y.UserId == userId && y.GuildId == guildId)
 | 
			
		||||
                               .Select(y => y.Xp + y.AwardedXp)
 | 
			
		||||
                               .FirstOrDefault()
 | 
			
		||||
               )
 | 
			
		||||
               .Count() +
 | 
			
		||||
           1;
 | 
			
		||||
 | 
			
		||||
    public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
 | 
			
		||||
        => xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,21 +7,24 @@ namespace NadekoBot.Db;
 | 
			
		||||
 | 
			
		||||
public class WaifuInfoStats
 | 
			
		||||
{
 | 
			
		||||
    public string FullName { get; set; }
 | 
			
		||||
    public int Price { get; set; }
 | 
			
		||||
    public string ClaimerName { get; set; }
 | 
			
		||||
    public string AffinityName { get; set; }
 | 
			
		||||
    public int AffinityCount { get; set; }
 | 
			
		||||
    public int DivorceCount { get; set; }
 | 
			
		||||
    public int ClaimCount { get; set; }
 | 
			
		||||
    public List<WaifuItem> Items { get; set; }
 | 
			
		||||
    public List<string> Claims { get; set; }
 | 
			
		||||
    public List<string> Fans { get; set; }
 | 
			
		||||
    public string FullName { get; init; }
 | 
			
		||||
    public int Price { get; init; }
 | 
			
		||||
    public string ClaimerName { get; init; }
 | 
			
		||||
    public string AffinityName { get; init; }
 | 
			
		||||
    public int AffinityCount { get; init; }
 | 
			
		||||
    public int DivorceCount { get; init; }
 | 
			
		||||
    public int ClaimCount { get; init; }
 | 
			
		||||
    public List<WaifuItem> Items { get; init; }
 | 
			
		||||
    public List<string> Claims { get; init; }
 | 
			
		||||
    public List<string> Fans { get; init; }
 | 
			
		||||
}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
public static class WaifuExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static WaifuInfo ByWaifuUserId(this DbSet<WaifuInfo> waifus, ulong userId, Func<DbSet<WaifuInfo>, IQueryable<WaifuInfo>> includes = null)
 | 
			
		||||
    public static WaifuInfo ByWaifuUserId(
 | 
			
		||||
        this DbSet<WaifuInfo> waifus,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        Func<DbSet<WaifuInfo>, IQueryable<WaifuInfo>> includes = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (includes is null)
 | 
			
		||||
        {
 | 
			
		||||
@@ -51,31 +54,28 @@ public static class WaifuExtensions
 | 
			
		||||
            .Skip(skip)
 | 
			
		||||
            .Take(count)
 | 
			
		||||
            .Select(x => new WaifuLbResult
 | 
			
		||||
            {
 | 
			
		||||
                Affinity = x.Affinity == null ? null : x.Affinity.Username,
 | 
			
		||||
                AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
 | 
			
		||||
                Claimer = x.Claimer == null ? null : x.Claimer.Username,
 | 
			
		||||
                ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
 | 
			
		||||
                Username = x.Waifu.Username,
 | 
			
		||||
                Discrim = x.Waifu.Discriminator,
 | 
			
		||||
                Price = x.Price,
 | 
			
		||||
            })
 | 
			
		||||
                {
 | 
			
		||||
                    Affinity = x.Affinity == null ? null : x.Affinity.Username,
 | 
			
		||||
                    AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
 | 
			
		||||
                    Claimer = x.Claimer == null ? null : x.Claimer.Username,
 | 
			
		||||
                    ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
 | 
			
		||||
                    Username = x.Waifu.Username,
 | 
			
		||||
                    Discrim = x.Waifu.Discriminator,
 | 
			
		||||
                    Price = x.Price,
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .ToList();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
 | 
			
		||||
        => waifus
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => waifus.AsQueryable()
 | 
			
		||||
            .Where(x => x.ClaimerId != null)
 | 
			
		||||
            .Sum(x => x.Price);
 | 
			
		||||
 | 
			
		||||
    public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name)
 | 
			
		||||
        => waifus
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
        => waifus.AsQueryable()
 | 
			
		||||
            .AsNoTracking()
 | 
			
		||||
            .Where(x => x.Claimer.UserId == ownerId
 | 
			
		||||
                        && x.Waifu.Username + "#" + x.Waifu.Discriminator == name)
 | 
			
		||||
            .Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name)
 | 
			
		||||
            .Select(x => x.Waifu.UserId)
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
@@ -83,74 +83,64 @@ public static class WaifuExtensions
 | 
			
		||||
    {
 | 
			
		||||
        ctx.Database.ExecuteSqlInterpolated($@"
 | 
			
		||||
INSERT OR IGNORE INTO WaifuInfo (AffinityId, ClaimerId, Price, WaifuId)
 | 
			
		||||
VALUES ({null}, {null}, {1}, (SELECT Id FROM DiscordUser WHERE UserId={userId}));");
 | 
			
		||||
VALUES ({null}, {null}, {1}, (SELECT Id FROM DiscordUser WHERE UserId={userId}));"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        var toReturn = ctx.WaifuInfo
 | 
			
		||||
            .AsQueryable()
 | 
			
		||||
            .Where(w => w.WaifuId == ctx.Set<DiscordUser>()
 | 
			
		||||
                .AsQueryable()
 | 
			
		||||
                .Where(u => u.UserId == userId)
 | 
			
		||||
                .Select(u => u.Id).FirstOrDefault())
 | 
			
		||||
        var toReturn = ctx.WaifuInfo.AsQueryable()
 | 
			
		||||
            .Where(w => w.WaifuId ==
 | 
			
		||||
                        ctx.Set<DiscordUser>()
 | 
			
		||||
                            .AsQueryable()
 | 
			
		||||
                            .Where(u => u.UserId == userId)
 | 
			
		||||
                            .Select(u => u.Id)
 | 
			
		||||
                            .FirstOrDefault()
 | 
			
		||||
            )
 | 
			
		||||
            .Select(w => new WaifuInfoStats
 | 
			
		||||
            {
 | 
			
		||||
                FullName = ctx.Set<DiscordUser>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(u => u.UserId == userId)
 | 
			
		||||
                    .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                    .FirstOrDefault(),
 | 
			
		||||
 | 
			
		||||
                AffinityCount = ctx.Set<WaifuUpdate>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Count(x => x.UserId == w.WaifuId &&
 | 
			
		||||
                                x.UpdateType == WaifuUpdateType.AffinityChanged &&
 | 
			
		||||
                                x.NewId != null),
 | 
			
		||||
 | 
			
		||||
                AffinityName = ctx.Set<DiscordUser>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(u => u.Id == w.AffinityId)
 | 
			
		||||
                    .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                    .FirstOrDefault(),
 | 
			
		||||
 | 
			
		||||
                ClaimCount = ctx.WaifuInfo
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Count(x => x.ClaimerId == w.WaifuId),
 | 
			
		||||
 | 
			
		||||
                ClaimerName = ctx.Set<DiscordUser>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Where(u => u.Id == w.ClaimerId)
 | 
			
		||||
                    .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                    .FirstOrDefault(),
 | 
			
		||||
 | 
			
		||||
                DivorceCount = ctx
 | 
			
		||||
                    .Set<WaifuUpdate>()
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Count(x => x.OldId == w.WaifuId &&
 | 
			
		||||
                                x.NewId == null &&
 | 
			
		||||
                                x.UpdateType == WaifuUpdateType.Claimed),
 | 
			
		||||
 | 
			
		||||
                Price = w.Price,
 | 
			
		||||
 | 
			
		||||
                Claims = ctx.WaifuInfo
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Include(x => x.Waifu)
 | 
			
		||||
                    .Where(x => x.ClaimerId == w.WaifuId)
 | 
			
		||||
                    .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator)
 | 
			
		||||
                    .ToList(),
 | 
			
		||||
 | 
			
		||||
                Fans = ctx.WaifuInfo
 | 
			
		||||
                    .AsQueryable()
 | 
			
		||||
                    .Include(x => x.Waifu)
 | 
			
		||||
                    .Where(x => x.AffinityId == w.WaifuId)
 | 
			
		||||
                    .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator)
 | 
			
		||||
                    .ToList(),
 | 
			
		||||
                    
 | 
			
		||||
                Items = w.Items,
 | 
			
		||||
            })
 | 
			
		||||
                {
 | 
			
		||||
                    FullName = ctx.Set<DiscordUser>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .Where(u => u.UserId == userId)
 | 
			
		||||
                        .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                        .FirstOrDefault(),
 | 
			
		||||
                    AffinityCount = ctx.Set<WaifuUpdate>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .Count(x => x.UserId == w.WaifuId &&
 | 
			
		||||
                                    x.UpdateType == WaifuUpdateType.AffinityChanged &&
 | 
			
		||||
                                    x.NewId != null
 | 
			
		||||
                        ),
 | 
			
		||||
                    AffinityName = ctx.Set<DiscordUser>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .Where(u => u.Id == w.AffinityId)
 | 
			
		||||
                        .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                        .FirstOrDefault(),
 | 
			
		||||
                    ClaimCount = ctx.WaifuInfo.AsQueryable()
 | 
			
		||||
                        .Count(x => x.ClaimerId == w.WaifuId),
 | 
			
		||||
                    ClaimerName = ctx.Set<DiscordUser>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .Where(u => u.Id == w.ClaimerId)
 | 
			
		||||
                        .Select(u => u.Username + "#" + u.Discriminator)
 | 
			
		||||
                        .FirstOrDefault(),
 | 
			
		||||
                    DivorceCount = ctx.Set<WaifuUpdate>()
 | 
			
		||||
                        .AsQueryable()
 | 
			
		||||
                        .Count(x => x.OldId == w.WaifuId && x.NewId == null && x.UpdateType == WaifuUpdateType.Claimed),
 | 
			
		||||
                    Price = w.Price,
 | 
			
		||||
                    Claims = ctx.WaifuInfo.AsQueryable()
 | 
			
		||||
                        .Include(x => x.Waifu)
 | 
			
		||||
                        .Where(x => x.ClaimerId == w.WaifuId)
 | 
			
		||||
                        .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator)
 | 
			
		||||
                        .ToList(),
 | 
			
		||||
                    Fans = ctx.WaifuInfo.AsQueryable()
 | 
			
		||||
                        .Include(x => x.Waifu)
 | 
			
		||||
                        .Where(x => x.AffinityId == w.WaifuId)
 | 
			
		||||
                        .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator)
 | 
			
		||||
                        .ToList(),
 | 
			
		||||
                    Items = w.Items,
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (toReturn is null)
 | 
			
		||||
            return null;
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        return toReturn;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,17 +14,24 @@ public static class WarningExtensions
 | 
			
		||||
        return query.ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool Forgive(this DbSet<Warning> warnings, ulong guildId, ulong userId, string mod, int index)
 | 
			
		||||
    public static bool Forgive(
 | 
			
		||||
        this DbSet<Warning> warnings,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        string mod,
 | 
			
		||||
        int index)
 | 
			
		||||
    {
 | 
			
		||||
        if (index < 0)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(index));
 | 
			
		||||
 | 
			
		||||
        var warn = warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId)
 | 
			
		||||
        var warn = warnings.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == guildId && x.UserId == userId)
 | 
			
		||||
            .OrderByDescending(x => x.DateAdded)
 | 
			
		||||
            .Skip(index)
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (warn is null || warn.Forgiven)
 | 
			
		||||
        if (warn is null ||
 | 
			
		||||
            warn.Forgiven)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        warn.Forgiven = true;
 | 
			
		||||
@@ -32,17 +39,25 @@ public static class WarningExtensions
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task ForgiveAll(this DbSet<Warning> warnings, ulong guildId, ulong userId, string mod)
 | 
			
		||||
        => await warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId)
 | 
			
		||||
    public static async Task ForgiveAll(
 | 
			
		||||
        this DbSet<Warning> warnings,
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        string mod)
 | 
			
		||||
        => await warnings.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == guildId && x.UserId == userId)
 | 
			
		||||
            .ForEachAsync(x =>
 | 
			
		||||
            {
 | 
			
		||||
                if (x.Forgiven != true)
 | 
			
		||||
                {
 | 
			
		||||
                    x.Forgiven = true;
 | 
			
		||||
                    x.ForgivenBy = mod;
 | 
			
		||||
                    if (x.Forgiven != true)
 | 
			
		||||
                    {
 | 
			
		||||
                        x.Forgiven = true;
 | 
			
		||||
                        x.ForgivenBy = mod;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
    public static Warning[] GetForGuild(this DbSet<Warning> warnings, ulong id)
 | 
			
		||||
        => warnings.AsQueryable().Where(x => x.GuildId == id).ToArray();
 | 
			
		||||
        => warnings.AsQueryable()
 | 
			
		||||
            .Where(x => x.GuildId == id)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
}
 | 
			
		||||
@@ -43,8 +43,8 @@ public partial class Administration
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public async Task LanguageSet()
 | 
			
		||||
            => await ReplyConfirmLocalizedAsync(strs.lang_set_show(
 | 
			
		||||
                Format.Bold(_cultureInfo.ToString()),
 | 
			
		||||
                Format.Bold(_cultureInfo.NativeName)));
 | 
			
		||||
                Format.Bold(Culture.ToString()),
 | 
			
		||||
                Format.Bold(Culture.NativeName)));
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Aliases]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1002,8 +1002,7 @@ public sealed class LogCommandService : ILogCommandService
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var msg = optMsg.Value as IUserMessage;
 | 
			
		||||
                if (msg is null || msg.IsAuthor(_client))
 | 
			
		||||
                if (optMsg.Value is not IUserMessage msg || msg.IsAuthor(_client))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (_ignoreMessageIds.Contains(msg.Id))
 | 
			
		||||
@@ -1058,8 +1057,7 @@ public sealed class LogCommandService : ILogCommandService
 | 
			
		||||
                if (imsg2 is not IUserMessage after || after.IsAuthor(_client))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var before = (optmsg.HasValue ? optmsg.Value : null) as IUserMessage;
 | 
			
		||||
                if (before is null)
 | 
			
		||||
                if ((optmsg.HasValue ? optmsg.Value : null) is not IUserMessage before)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (ch is not ITextChannel channel)
 | 
			
		||||
 
 | 
			
		||||
@@ -167,9 +167,7 @@ public class VcRoleService : INService
 | 
			
		||||
    private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState,
 | 
			
		||||
        SocketVoiceState newState)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        var gusr = usr as SocketGuildUser;
 | 
			
		||||
        if (gusr is null)
 | 
			
		||||
        if (usr is not SocketGuildUser gusr)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        var oldVc = oldState.VoiceChannel;
 | 
			
		||||
 
 | 
			
		||||
@@ -465,7 +465,7 @@ public partial class Administration
 | 
			
		||||
                .AddField(GetText(strs.duration),
 | 
			
		||||
                    time.Time.Humanize(3,
 | 
			
		||||
                        minUnit: TimeUnit.Minute,
 | 
			
		||||
                        culture: _cultureInfo),
 | 
			
		||||
                        culture: Culture),
 | 
			
		||||
                    true);
 | 
			
		||||
 | 
			
		||||
            if (dmFailed)
 | 
			
		||||
 
 | 
			
		||||
@@ -152,8 +152,7 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
        {
 | 
			
		||||
            if (_emote.Name != r.Emote.Name)
 | 
			
		||||
                return;
 | 
			
		||||
            var gu = (r.User.IsSpecified ? r.User.Value : null) as IGuildUser;
 | 
			
		||||
            if (gu is null // no unknown users, as they could be bots, or alts
 | 
			
		||||
            if ((r.User.IsSpecified ? r.User.Value : null) is not IGuildUser gu // no unknown users, as they could be bots, or alts
 | 
			
		||||
                || msg.Id != _msg.Id // same message
 | 
			
		||||
                || gu.IsBot // no bots
 | 
			
		||||
                || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
    private string n(long cur)
 | 
			
		||||
    {
 | 
			
		||||
        var flowersCi = (CultureInfo)_cultureInfo.Clone();
 | 
			
		||||
        var flowersCi = (CultureInfo)Culture.Clone();
 | 
			
		||||
        flowersCi.NumberFormat.CurrencySymbol = CurrencySign;
 | 
			
		||||
        Log.Information(string.Join(",", flowersCi.NumberFormat.NativeDigits));
 | 
			
		||||
        return cur.ToString("C0", flowersCi);
 | 
			
		||||
@@ -59,12 +59,12 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        }
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
            .WithTitle(GetText(strs.economy_state))
 | 
			
		||||
            .AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", _cultureInfo) + CurrencySign)
 | 
			
		||||
            .AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", Culture) + CurrencySign)
 | 
			
		||||
            .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
 | 
			
		||||
            .AddField(GetText(strs.currency_planted), (BigInteger)ec.Planted)
 | 
			
		||||
            .AddField(GetText(strs.owned_waifus_total), (BigInteger)ec.Waifus + CurrencySign)
 | 
			
		||||
            .AddField(GetText(strs.bot_currency), n(ec.Bot))
 | 
			
		||||
            .AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _cultureInfo) + CurrencySign)
 | 
			
		||||
            .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).ConfigureAwait(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,7 @@ public class CurrencyEventsService : INService
 | 
			
		||||
        EventOptions opts, Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embed)
 | 
			
		||||
    {
 | 
			
		||||
        var g = _client.GetGuild(guildId);
 | 
			
		||||
        var ch = g?.GetChannel(channelId) as SocketTextChannel;
 | 
			
		||||
        if (ch is null)
 | 
			
		||||
        if (g?.GetChannel(channelId) is not SocketTextChannel ch)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        ICurrencyEvent ce;
 | 
			
		||||
 
 | 
			
		||||
@@ -159,8 +159,7 @@ public class PlantPickService : INService
 | 
			
		||||
 | 
			
		||||
    private Task PotentialFlowerGeneration(IUserMessage imsg)
 | 
			
		||||
    {
 | 
			
		||||
        var msg = imsg as SocketUserMessage;
 | 
			
		||||
        if (msg is null || msg.Author.IsBot)
 | 
			
		||||
        if (imsg is not SocketUserMessage msg || msg.Author.IsBot)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        if (imsg.Channel is not ITextChannel channel)
 | 
			
		||||
 
 | 
			
		||||
@@ -196,8 +196,7 @@ public class TriviaGame
 | 
			
		||||
 | 
			
		||||
                var umsg = imsg as SocketUserMessage;
 | 
			
		||||
 | 
			
		||||
                var textChannel = umsg?.Channel as ITextChannel;
 | 
			
		||||
                if (textChannel is null || textChannel.Guild != Guild)
 | 
			
		||||
                if (umsg?.Channel is not ITextChannel textChannel || textChannel.Guild != Guild)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var guildUser = (IGuildUser)umsg.Author;
 | 
			
		||||
 
 | 
			
		||||
@@ -133,8 +133,7 @@ public class TypingGame
 | 
			
		||||
            {
 | 
			
		||||
                if (imsg.Author.IsBot)
 | 
			
		||||
                    return;
 | 
			
		||||
                var msg = imsg as SocketUserMessage;
 | 
			
		||||
                if (msg is null)
 | 
			
		||||
                if (imsg is not SocketUserMessage msg)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (this.Channel is null || this.Channel.Id != msg.Channel.Id) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -97,9 +97,8 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
            var _ = Task.Run(() =>
 | 
			
		||||
            {
 | 
			
		||||
                var guild = (channel as ITextChannel)?.Guild;
 | 
			
		||||
                var usrMsg = newMsg as IUserMessage;
 | 
			
		||||
 | 
			
		||||
                if (guild is null || usrMsg is null)
 | 
			
		||||
                if (guild is null || newMsg is not IUserMessage usrMsg)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                return RunBehavior(guild, usrMsg);
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ public partial class Searches
 | 
			
		||||
                        .WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc)
 | 
			
		||||
                            ? kvp.Value.ShortDesc
 | 
			
		||||
                            : kvp.Value.Desc)
 | 
			
		||||
                        .AddField(GetText(strs.rating), kvp.Value.Rating.ToString(_cultureInfo), true));
 | 
			
		||||
                        .AddField(GetText(strs.rating), kvp.Value.Rating.ToString(Culture), true));
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -626,9 +626,7 @@ public class SearchesService : INService
 | 
			
		||||
 | 
			
		||||
        var results = elems.Select(elem =>
 | 
			
		||||
            {
 | 
			
		||||
                var anchor = elem.QuerySelector(".result__a") as IHtmlAnchorElement;
 | 
			
		||||
 | 
			
		||||
                if (anchor is null)
 | 
			
		||||
                if (elem.QuerySelector(".result__a") is not IHtmlAnchorElement anchor)
 | 
			
		||||
                    return null;
 | 
			
		||||
 | 
			
		||||
                var href = anchor.Href;
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,7 @@ public partial class Utility
 | 
			
		||||
                    var diff = when - DateTime.UtcNow;
 | 
			
		||||
                    embed.AddField(
 | 
			
		||||
                        $"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC " +
 | 
			
		||||
                        $"(in {diff.Humanize(2, minUnit: TimeUnit.Minute, culture: _cultureInfo)})",
 | 
			
		||||
                        $"(in {diff.Humanize(2, minUnit: TimeUnit.Minute, culture: Culture)})",
 | 
			
		||||
                        $@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
 | 
			
		||||
`TargetId:` {rem.ChannelId}
 | 
			
		||||
`Message:` {rem.Message?.TrimTo(50)}", false);
 | 
			
		||||
@@ -230,7 +230,7 @@ public partial class Utility
 | 
			
		||||
                    "⏰ " + GetText(strs.remind(
 | 
			
		||||
                        Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
 | 
			
		||||
                        Format.Bold(message),
 | 
			
		||||
                        ts.Humanize(3, minUnit: TimeUnit.Second, culture: _cultureInfo),
 | 
			
		||||
                        ts.Humanize(3, minUnit: TimeUnit.Second, culture: Culture),
 | 
			
		||||
                        gTime, gTime))).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
 
 | 
			
		||||
@@ -106,14 +106,6 @@ public partial class Xp : NadekoModule<XpService>
 | 
			
		||||
        }, allRewards.Count, 9);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum AddRemove
 | 
			
		||||
    {
 | 
			
		||||
        Add = 0,
 | 
			
		||||
        Remove = 1,
 | 
			
		||||
        Rm = 1,
 | 
			
		||||
        Rem = 1,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [NadekoCommand, Aliases]
 | 
			
		||||
    [UserPerm(GuildPerm.Administrator)]
 | 
			
		||||
    [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@
 | 
			
		||||
    <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
 | 
			
		||||
    <OutputType>exe</OutputType>
 | 
			
		||||
    <ApplicationIcon>nadeko_icon.ico</ApplicationIcon>
 | 
			
		||||
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,16 @@ namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
        
 | 
			
		||||
    private readonly ConnectionMultiplexer _con;
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private readonly HttpClient _http;
 | 
			
		||||
    private readonly string _imagesPath;
 | 
			
		||||
 | 
			
		||||
    private IDatabase _db => _con.GetDatabase();
 | 
			
		||||
    private IDatabase Db
 | 
			
		||||
        => _con.GetDatabase();
 | 
			
		||||
 | 
			
		||||
    private const string _basePath = "data/";
 | 
			
		||||
    private const string _cardsPath = $"{_basePath}images/cards";
 | 
			
		||||
    private const string BASE_PATH = "data/";
 | 
			
		||||
    private const string CARDS_PATH = $"{BASE_PATH}images/cards";
 | 
			
		||||
 | 
			
		||||
    public ImageUrls ImageUrls { get; private set; }
 | 
			
		||||
 | 
			
		||||
@@ -35,42 +35,42 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
        XpBg
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<byte[]> Heads 
 | 
			
		||||
    public IReadOnlyList<byte[]> Heads
 | 
			
		||||
        => GetByteArrayData(ImageKeys.CoinHeads);
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<byte[]> Tails 
 | 
			
		||||
    public IReadOnlyList<byte[]> Tails
 | 
			
		||||
        => GetByteArrayData(ImageKeys.CoinTails);
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<byte[]> Dice 
 | 
			
		||||
    public IReadOnlyList<byte[]> Dice
 | 
			
		||||
        => GetByteArrayData(ImageKeys.Dice);
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<byte[]> SlotEmojis 
 | 
			
		||||
    public IReadOnlyList<byte[]> SlotEmojis
 | 
			
		||||
        => GetByteArrayData(ImageKeys.SlotEmojis);
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyList<byte[]> Currency 
 | 
			
		||||
    public IReadOnlyList<byte[]> Currency
 | 
			
		||||
        => GetByteArrayData(ImageKeys.Currency);
 | 
			
		||||
 | 
			
		||||
    public byte[] SlotBackground 
 | 
			
		||||
    public byte[] SlotBackground
 | 
			
		||||
        => GetByteData(ImageKeys.SlotBg);
 | 
			
		||||
 | 
			
		||||
    public byte[] RategirlMatrix 
 | 
			
		||||
    public byte[] RategirlMatrix
 | 
			
		||||
        => GetByteData(ImageKeys.RategirlMatrix);
 | 
			
		||||
 | 
			
		||||
    public byte[] RategirlDot 
 | 
			
		||||
    public byte[] RategirlDot
 | 
			
		||||
        => GetByteData(ImageKeys.RategirlDot);
 | 
			
		||||
 | 
			
		||||
    public byte[] XpBackground 
 | 
			
		||||
    public byte[] XpBackground
 | 
			
		||||
        => GetByteData(ImageKeys.XpBg);
 | 
			
		||||
 | 
			
		||||
    public byte[] Rip 
 | 
			
		||||
    public byte[] Rip
 | 
			
		||||
        => GetByteData(ImageKeys.RipBg);
 | 
			
		||||
 | 
			
		||||
    public byte[] RipOverlay 
 | 
			
		||||
    public byte[] RipOverlay
 | 
			
		||||
        => GetByteData(ImageKeys.RipOverlay);
 | 
			
		||||
 | 
			
		||||
    public byte[] GetCard(string key)
 | 
			
		||||
        // since cards are always local for now, don't cache them
 | 
			
		||||
        => File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg"));
 | 
			
		||||
        => File.ReadAllBytes(Path.Join(CARDS_PATH, key + ".jpg"));
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
@@ -85,7 +85,7 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
        _con = con;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _http = new();
 | 
			
		||||
        _imagesPath = Path.Combine(_basePath, "images.yml");
 | 
			
		||||
        _imagesPath = Path.Combine(BASE_PATH, "images.yml");
 | 
			
		||||
 | 
			
		||||
        Migrate();
 | 
			
		||||
 | 
			
		||||
@@ -95,58 +95,50 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
    private void Migrate()
 | 
			
		||||
    {
 | 
			
		||||
        // migrate to yml
 | 
			
		||||
        if (File.Exists(Path.Combine(_basePath, "images.json")))
 | 
			
		||||
        if (File.Exists(Path.Combine(BASE_PATH, "images.json")))
 | 
			
		||||
        {
 | 
			
		||||
            var oldFilePath = Path.Combine(_basePath, "images.json");
 | 
			
		||||
            var backupFilePath = Path.Combine(_basePath, "images.json.backup");
 | 
			
		||||
                
 | 
			
		||||
            var oldData = JsonConvert.DeserializeObject<OldImageUrls>(
 | 
			
		||||
                File.ReadAllText(oldFilePath));
 | 
			
		||||
            var oldFilePath = Path.Combine(BASE_PATH, "images.json");
 | 
			
		||||
            var backupFilePath = Path.Combine(BASE_PATH, "images.json.backup");
 | 
			
		||||
 | 
			
		||||
            var oldData = JsonConvert.DeserializeObject<OldImageUrls>(File.ReadAllText(oldFilePath));
 | 
			
		||||
 | 
			
		||||
            if (oldData is not null)
 | 
			
		||||
            {
 | 
			
		||||
                var newData = new ImageUrls()
 | 
			
		||||
                {
 | 
			
		||||
                    Coins = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Heads = oldData.Coins.Heads.Length == 1 && 
 | 
			
		||||
                                oldData.Coins.Heads[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
 | 
			
		||||
                            ? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
 | 
			
		||||
                            : oldData.Coins.Heads,
 | 
			
		||||
                        Tails = oldData.Coins.Tails.Length == 1 && 
 | 
			
		||||
                                oldData.Coins.Tails[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
 | 
			
		||||
                            ? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
 | 
			
		||||
                            : oldData.Coins.Tails,
 | 
			
		||||
                    },
 | 
			
		||||
                    Coins =
 | 
			
		||||
                        new()
 | 
			
		||||
                        {
 | 
			
		||||
                            Heads = oldData.Coins.Heads.Length == 1 &&
 | 
			
		||||
                                    oldData.Coins.Heads[0].ToString() ==
 | 
			
		||||
                                    "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
 | 
			
		||||
                                ? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
 | 
			
		||||
                                : oldData.Coins.Heads,
 | 
			
		||||
                            Tails = oldData.Coins.Tails.Length == 1 &&
 | 
			
		||||
                                    oldData.Coins.Tails[0].ToString() ==
 | 
			
		||||
                                    "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
 | 
			
		||||
                                ? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
 | 
			
		||||
                                : oldData.Coins.Tails,
 | 
			
		||||
                        },
 | 
			
		||||
                    Dice = oldData.Dice.Map(x => x.ToNewCdn()),
 | 
			
		||||
                    Currency = oldData.Currency.Map(x => x.ToNewCdn()),
 | 
			
		||||
                    Rategirl = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Dot = oldData.Rategirl.Dot.ToNewCdn(),
 | 
			
		||||
                        Matrix = oldData.Rategirl.Matrix.ToNewCdn()
 | 
			
		||||
                    },
 | 
			
		||||
                    Rip = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Bg = oldData.Rip.Bg.ToNewCdn(),
 | 
			
		||||
                        Overlay = oldData.Rip.Overlay.ToNewCdn(),
 | 
			
		||||
                    },
 | 
			
		||||
                    Rategirl =
 | 
			
		||||
                        new()
 | 
			
		||||
                        {
 | 
			
		||||
                            Dot = oldData.Rategirl.Dot.ToNewCdn(), Matrix = oldData.Rategirl.Matrix.ToNewCdn()
 | 
			
		||||
                        },
 | 
			
		||||
                    Rip = new() { Bg = oldData.Rip.Bg.ToNewCdn(), Overlay = oldData.Rip.Overlay.ToNewCdn(), },
 | 
			
		||||
                    Slots = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Bg = new("https://cdn.nadeko.bot/slots/slots_bg.png"),
 | 
			
		||||
                        Emojis = new[]
 | 
			
		||||
                        {
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/0.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/1.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/2.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/3.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/4.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/5.png"
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/0.png", "https://cdn.nadeko.bot/slots/1.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/2.png", "https://cdn.nadeko.bot/slots/3.png",
 | 
			
		||||
                            "https://cdn.nadeko.bot/slots/4.png", "https://cdn.nadeko.bot/slots/5.png"
 | 
			
		||||
                        }.Map(x => new Uri(x))
 | 
			
		||||
                    },
 | 
			
		||||
                    Xp = new()
 | 
			
		||||
                    {
 | 
			
		||||
                        Bg = oldData.Xp.Bg.ToNewCdn(),
 | 
			
		||||
                    },
 | 
			
		||||
                    Xp = new() { Bg = oldData.Xp.Bg.ToNewCdn(), },
 | 
			
		||||
                    Version = 2,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
@@ -216,25 +208,25 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
        if (data is null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await _db.StringSetAsync(GetRedisKey(key), data);
 | 
			
		||||
        await Db.StringSetAsync(GetRedisKey(key), data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task Load(ImageKeys key, Uri[] uris)
 | 
			
		||||
    {
 | 
			
		||||
        await _db.KeyDeleteAsync(GetRedisKey(key));
 | 
			
		||||
        await Db.KeyDeleteAsync(GetRedisKey(key));
 | 
			
		||||
        var imageData = await Task.WhenAll(uris.Select(GetImageData));
 | 
			
		||||
        var vals = imageData
 | 
			
		||||
            .Where(x => x is not null)
 | 
			
		||||
            .Select(x => (RedisValue)x)
 | 
			
		||||
            .ToArray();
 | 
			
		||||
        var vals = imageData.Where(x => x is not null).Select(x => (RedisValue)x).ToArray();
 | 
			
		||||
 | 
			
		||||
        await Db.ListRightPushAsync(GetRedisKey(key), vals);
 | 
			
		||||
 | 
			
		||||
        await _db.ListRightPushAsync(GetRedisKey(key), vals);
 | 
			
		||||
            
 | 
			
		||||
        if (uris.Length != vals.Length)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n" +
 | 
			
		||||
                            "Some of the supplied URIs are either unavailable or invalid.", 
 | 
			
		||||
                vals.Length, uris.Length, key);
 | 
			
		||||
                            "Some of the supplied URIs are either unavailable or invalid",
 | 
			
		||||
                vals.Length,
 | 
			
		||||
                uris.Length,
 | 
			
		||||
                key
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -264,24 +256,23 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> AllKeysExist()
 | 
			
		||||
    {
 | 
			
		||||
        var tasks = await Task.WhenAll(GetAllKeys()
 | 
			
		||||
            .Select(x => _db.KeyExistsAsync(GetRedisKey(x))));
 | 
			
		||||
        var tasks = await Task.WhenAll(GetAllKeys().Select(x => Db.KeyExistsAsync(GetRedisKey(x))));
 | 
			
		||||
 | 
			
		||||
        return tasks.All(exist => exist);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IEnumerable<ImageKeys> GetAllKeys() =>
 | 
			
		||||
        Enum.GetValues<ImageKeys>();
 | 
			
		||||
    private IEnumerable<ImageKeys> GetAllKeys()
 | 
			
		||||
        => Enum.GetValues<ImageKeys>();
 | 
			
		||||
 | 
			
		||||
    private byte[][] GetByteArrayData(ImageKeys key)
 | 
			
		||||
        => _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
 | 
			
		||||
        => Db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
 | 
			
		||||
 | 
			
		||||
    private byte[] GetByteData(ImageKeys key)
 | 
			
		||||
        => _db.StringGet(GetRedisKey(key));
 | 
			
		||||
        => Db.StringGet(GetRedisKey(key));
 | 
			
		||||
 | 
			
		||||
    private RedisKey GetRedisKey(ImageKeys key) 
 | 
			
		||||
    private RedisKey GetRedisKey(ImageKeys key)
 | 
			
		||||
        => _creds.RedisKey() + "_image_" + key;
 | 
			
		||||
}
 | 
			
		||||
@@ -10,35 +10,33 @@ public class RedisLocalDataCache : ILocalDataCache
 | 
			
		||||
    private readonly ConnectionMultiplexer _con;
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
    private IDatabase _db => _con.GetDatabase();
 | 
			
		||||
 | 
			
		||||
    private const string pokemonAbilitiesFile = "data/pokemon/pokemon_abilities.json";
 | 
			
		||||
    private const string pokemonListFile = "data/pokemon/pokemon_list.json";
 | 
			
		||||
    private const string pokemonMapPath = "data/pokemon/name-id_map.json";
 | 
			
		||||
    private const string questionsFile = "data/trivia_questions.json";
 | 
			
		||||
    private const string POKEMON_ABILITIES_FILE = "data/pokemon/pokemon_abilities.json";
 | 
			
		||||
    private const string POKEMON_LIST_FILE = "data/pokemon/pokemon_list.json";
 | 
			
		||||
    private const string POKEMON_MAP_PATH = "data/pokemon/name-id_map.json";
 | 
			
		||||
    private const string QUESTIONS_FILE = "data/trivia_questions.json";
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyDictionary<string, SearchPokemon> Pokemons
 | 
			
		||||
    {
 | 
			
		||||
        get => Get<Dictionary<string, SearchPokemon>>("pokemon_list");
 | 
			
		||||
        private set => Set("pokemon_list", value);
 | 
			
		||||
        private init => Set("pokemon_list", value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
 | 
			
		||||
    {
 | 
			
		||||
        get => Get<Dictionary<string, SearchPokemonAbility>>("pokemon_abilities");
 | 
			
		||||
        private set => Set("pokemon_abilities", value);
 | 
			
		||||
        private init => Set("pokemon_abilities", value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TriviaQuestion[] TriviaQuestions
 | 
			
		||||
    {
 | 
			
		||||
        get => Get<TriviaQuestion[]>("trivia_questions");
 | 
			
		||||
        private set => Set("trivia_questions", value);
 | 
			
		||||
        private init => Set("trivia_questions", value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyDictionary<int, string> PokemonMap
 | 
			
		||||
    {
 | 
			
		||||
        get => Get<Dictionary<int, string>>("pokemon_map");
 | 
			
		||||
        private set => Set("pokemon_map", value);
 | 
			
		||||
        private init => Set("pokemon_map", value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, DiscordSocketClient client)
 | 
			
		||||
@@ -49,29 +47,33 @@ public class RedisLocalDataCache : ILocalDataCache
 | 
			
		||||
 | 
			
		||||
        if (shardId == 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!File.Exists(pokemonListFile))
 | 
			
		||||
            if (!File.Exists(POKEMON_LIST_FILE))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"{pokemonListFile} is missing. Pokemon abilities not loaded");
 | 
			
		||||
                Log.Warning($"{POKEMON_LIST_FILE} is missing. Pokemon abilities not loaded");
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Pokemons = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(pokemonListFile));
 | 
			
		||||
                Pokemons =
 | 
			
		||||
                    JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(POKEMON_LIST_FILE));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!File.Exists(pokemonAbilitiesFile))
 | 
			
		||||
            if (!File.Exists(POKEMON_ABILITIES_FILE))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning($"{pokemonAbilitiesFile} is missing. Pokemon abilities not loaded.");
 | 
			
		||||
                Log.Warning($"{POKEMON_ABILITIES_FILE} is missing. Pokemon abilities not loaded.");
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                PokemonAbilities = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(File.ReadAllText(pokemonAbilitiesFile));
 | 
			
		||||
                PokemonAbilities =
 | 
			
		||||
                    JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(
 | 
			
		||||
                        File.ReadAllText(POKEMON_ABILITIES_FILE)
 | 
			
		||||
                    );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
 | 
			
		||||
                PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
 | 
			
		||||
                    .ToDictionary(x => x.Id, x => x.Name);
 | 
			
		||||
                TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(QUESTIONS_FILE));
 | 
			
		||||
                PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(POKEMON_MAP_PATH))
 | 
			
		||||
                    ?.ToDictionary(x => x.Id, x => x.Name) ?? new();
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
@@ -81,9 +83,10 @@ public class RedisLocalDataCache : ILocalDataCache
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private T Get<T>(string key) where T : class
 | 
			
		||||
        => JsonConvert.DeserializeObject<T>(_db.StringGet($"{_creds.RedisKey()}_localdata_{key}"));
 | 
			
		||||
    private T Get<T>(string key)
 | 
			
		||||
        where T : class
 | 
			
		||||
        => JsonConvert.DeserializeObject<T>(_con.GetDatabase().StringGet($"{_creds.RedisKey()}_localdata_{key}"));
 | 
			
		||||
 | 
			
		||||
    private void Set(string key, object obj)
 | 
			
		||||
        => _db.StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
 | 
			
		||||
        => _con.GetDatabase().StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
 | 
			
		||||
}
 | 
			
		||||
@@ -11,8 +11,7 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards)
 | 
			
		||||
        => totalShards <= 1
 | 
			
		||||
            ? services
 | 
			
		||||
                .AddSingleton<IStringsSource, LocalFileStringsSource>()
 | 
			
		||||
            ? services.AddSingleton<IStringsSource, LocalFileStringsSource>()
 | 
			
		||||
                .AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
 | 
			
		||||
                .AddSingleton<IBotStrings, BotStrings>()
 | 
			
		||||
            : services.AddSingleton<IStringsSource, LocalFileStringsSource>()
 | 
			
		||||
@@ -25,7 +24,8 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
 | 
			
		||||
        foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
 | 
			
		||||
        {
 | 
			
		||||
            if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
 | 
			
		||||
            if (type.BaseType?.IsGenericType == true &&
 | 
			
		||||
                type.BaseType.GetGenericTypeDefinition() == baseType)
 | 
			
		||||
            {
 | 
			
		||||
                services.AddSingleton(type);
 | 
			
		||||
                services.AddSingleton(x => (IConfigService)x.GetRequiredService(type));
 | 
			
		||||
@@ -39,8 +39,7 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
        => services.AddSealedSubclassesOf(typeof(IConfigMigrator));
 | 
			
		||||
 | 
			
		||||
    public static IServiceCollection AddMusic(this IServiceCollection services)
 | 
			
		||||
        => services
 | 
			
		||||
            .AddSingleton<IMusicService, MusicService>()
 | 
			
		||||
        => services.AddSingleton<IMusicService, MusicService>()
 | 
			
		||||
            .AddSingleton<ITrackResolveProvider, TrackResolveProvider>()
 | 
			
		||||
            .AddSingleton<IYoutubeResolver, YtdlYoutubeResolver>()
 | 
			
		||||
            .AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
 | 
			
		||||
@@ -49,14 +48,13 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
            .AddSingleton<ITrackCacher, RedisTrackCacher>()
 | 
			
		||||
            .AddSingleton<YtLoader>()
 | 
			
		||||
            .AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    // consider using scrutor, because slightly different versions
 | 
			
		||||
    // of this might be needed in several different places
 | 
			
		||||
    public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType)
 | 
			
		||||
    {
 | 
			
		||||
        var subTypes = Assembly.GetCallingAssembly()
 | 
			
		||||
            .ExportedTypes
 | 
			
		||||
            .Where(type => type.IsSealed && baseType.IsAssignableFrom(type));
 | 
			
		||||
            .ExportedTypes.Where(type => type.IsSealed && baseType.IsAssignableFrom(type));
 | 
			
		||||
 | 
			
		||||
        foreach (var subType in subTypes)
 | 
			
		||||
        {
 | 
			
		||||
		Reference in New Issue
	
	Block a user