mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Removing bloat, fixing file names
This commit is contained in:
		@@ -149,7 +149,7 @@ public sealed class Bot
 | 
			
		||||
        if (Client.ShardId == 0)
 | 
			
		||||
            ApplyConfigMigrations();
 | 
			
		||||
 | 
			
		||||
        _ = LoadTypeReaders(typeof(Bot).Assembly);
 | 
			
		||||
        LoadTypeReaders(typeof(Bot).Assembly);
 | 
			
		||||
 | 
			
		||||
        sw.Stop();
 | 
			
		||||
        Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
 | 
			
		||||
@@ -163,29 +163,23 @@ public sealed class Bot
 | 
			
		||||
            migrator.EnsureMigrated();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IEnumerable<object> LoadTypeReaders(Assembly assembly)
 | 
			
		||||
    private void LoadTypeReaders(Assembly assembly)
 | 
			
		||||
    {
 | 
			
		||||
        var allTypes = assembly.GetTypes();
 | 
			
		||||
 | 
			
		||||
        var filteredTypes = allTypes.Where(x => x.IsSubclassOf(typeof(TypeReader))
 | 
			
		||||
                                                && x.BaseType?.GetGenericArguments().Length > 0
 | 
			
		||||
                                                && !x.IsAbstract);
 | 
			
		||||
 | 
			
		||||
        var toReturn = new List<object>();
 | 
			
		||||
        var filteredTypes = assembly.GetTypes()
 | 
			
		||||
            .Where(x => x.IsSubclassOf(typeof(TypeReader))
 | 
			
		||||
                        && x.BaseType?.GetGenericArguments().Length > 0
 | 
			
		||||
                        && !x.IsAbstract);
 | 
			
		||||
 
 | 
			
		||||
        foreach (var ft in filteredTypes)
 | 
			
		||||
        {
 | 
			
		||||
            var baseType = ft.BaseType;
 | 
			
		||||
            if (baseType is null)
 | 
			
		||||
                continue;
 | 
			
		||||
            
 | 
			
		||||
            var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
 | 
			
		||||
 | 
			
		||||
             
 | 
			
		||||
            var typeReader = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
 | 
			
		||||
            var typeArgs = baseType.GetGenericArguments();
 | 
			
		||||
            _commandService.AddTypeReader(typeArgs[0], x);
 | 
			
		||||
            toReturn.Add(x);
 | 
			
		||||
            _commandService.AddTypeReader(typeArgs[0], typeReader);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return toReturn;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task LoginAsync(string token)
 | 
			
		||||
@@ -319,7 +313,6 @@ public sealed class Bot
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                Console.WriteLine(toExec.GetType().FullName);
 | 
			
		||||
                await toExec.OnReadyAsync();
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ namespace NadekoBot.Common;
 | 
			
		||||
/// Classed marked with this attribute will not be added to the service provider 
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Class)]
 | 
			
		||||
public class DontAddToIocContainerAttribute : Attribute
 | 
			
		||||
public class DIIgnoreAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
@@ -18,21 +18,4 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
 | 
			
		||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
 | 
			
		||||
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
{
 | 
			
		||||
    public override Task<PreconditionResult> CheckPermissionsAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        CommandInfo command,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
#if GLOBAL_NADEKO || DEBUG
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#else
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/NadekoBot/Common/Attributes/OnlyPublicBotAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/NadekoBot/Common/Attributes/OnlyPublicBotAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
 | 
			
		||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
 | 
			
		||||
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
{
 | 
			
		||||
    public override Task<PreconditionResult> CheckPermissionsAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        CommandInfo command,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
#if GLOBAL_NADEKO || DEBUG
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#else
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
 | 
			
		||||
[DontAddToIocContainer]
 | 
			
		||||
[DIIgnore]
 | 
			
		||||
public sealed class BehaviorAdapter : ICustomBehavior
 | 
			
		||||
{
 | 
			
		||||
    private readonly WeakReference<Snek> _snekWr;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
public sealed class ShmartBankAmountTypeReader : NadekoTypeReader<ShmartBankAmount>
 | 
			
		||||
{
 | 
			
		||||
    private readonly IBankService _bank;
 | 
			
		||||
    private readonly ShmartBankInputAmountReader _tr;
 | 
			
		||||
 | 
			
		||||
    public ShmartBankAmountTypeReader(IBankService bank, DbService db, GamblingConfigService gambling)
 | 
			
		||||
    {
 | 
			
		||||
        _bank = bank;
 | 
			
		||||
        _tr = new ShmartBankInputAmountReader(bank, db, gambling);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async ValueTask<TypeReaderResult<ShmartBankAmount>> ReadAsync(ICommandContext ctx, string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
            return TypeReaderResult.FromError<ShmartBankAmount>(CommandError.ParseFailed, "Input is empty.");
 | 
			
		||||
 | 
			
		||||
        var result = await _tr.ReadAsync(ctx, input);
 | 
			
		||||
 | 
			
		||||
        if (result.TryPickT0(out var val, out var err))
 | 
			
		||||
        {
 | 
			
		||||
            return TypeReaderResult.FromSuccess<ShmartBankAmount>(new(val));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return TypeReaderResult.FromError<ShmartBankAmount>(CommandError.Unsuccessful, err.Value);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,29 +1,57 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
 | 
			
		||||
public sealed class BalanceTypeReader : TypeReader
 | 
			
		||||
{
 | 
			
		||||
    private readonly BaseShmartInputAmountReader _tr;
 | 
			
		||||
 | 
			
		||||
    public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling)
 | 
			
		||||
    public BalanceTypeReader(DbService db, GamblingConfigService gambling)
 | 
			
		||||
    {
 | 
			
		||||
        _tr = new BaseShmartInputAmountReader(db, gambling);
 | 
			
		||||
        _tr = new BaseShmartInputAmountReader(db, gambling); 
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async ValueTask<TypeReaderResult<ShmartNumber>> ReadAsync(ICommandContext ctx, string input)
 | 
			
		||||
    
 | 
			
		||||
    public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        string input,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
            return TypeReaderResult.FromError<ShmartNumber>(CommandError.ParseFailed, "Input is empty.");
 | 
			
		||||
 | 
			
		||||
        var result = await _tr.ReadAsync(ctx, input);
 | 
			
		||||
        var result = await _tr.ReadAsync(context, input);
 | 
			
		||||
 | 
			
		||||
        if (result.TryPickT0(out var val, out var err))
 | 
			
		||||
        {
 | 
			
		||||
            return TypeReaderResult.FromSuccess<ShmartNumber>(new(val));
 | 
			
		||||
            return Discord.Commands.TypeReaderResult.FromSuccess(val);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
        return TypeReaderResult.FromError<ShmartNumber>(CommandError.Unsuccessful, err.Value);
 | 
			
		||||
public sealed class BankBalanceTypeReader : TypeReader
 | 
			
		||||
{
 | 
			
		||||
    private readonly ShmartBankInputAmountReader _tr;
 | 
			
		||||
 | 
			
		||||
    public BankBalanceTypeReader(IBankService bank, DbService db, GamblingConfigService gambling)
 | 
			
		||||
    {
 | 
			
		||||
        _tr = new ShmartBankInputAmountReader(bank, db, gambling);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        string input,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        var result = await _tr.ReadAsync(context, input);
 | 
			
		||||
 | 
			
		||||
        if (result.TryPickT0(out var val, out var err))
 | 
			
		||||
        {
 | 
			
		||||
            return Discord.Commands.TypeReaderResult.FromSuccess(val);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
 | 
			
		||||
@@ -135,7 +136,7 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task JoinRace(ShmartNumber amount = default)
 | 
			
		||||
        public async Task JoinRace([OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetOptional(amount))
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
@@ -22,14 +23,14 @@ public partial class Gambling
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task BankDeposit(ShmartNumber amount)
 | 
			
		||||
        public async Task BankDeposit([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            if (await _bank.DepositAsync(ctx.User.Id, amount))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount.Value)));
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
@@ -38,14 +39,14 @@ public partial class Gambling
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task BankWithdraw(ShmartBankAmount amount)
 | 
			
		||||
        public async Task BankWithdraw([OverrideTypeReader(typeof(BankBalanceTypeReader))] long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount <= 0)
 | 
			
		||||
                return;
 | 
			
		||||
            
 | 
			
		||||
            if (await _bank.WithdrawAsync(ctx.User.Id, amount))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount.Amount)));
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount)));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common.Blackjack;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
@@ -30,7 +31,7 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task BlackJack(ShmartNumber amount)
 | 
			
		||||
        public async Task BlackJack([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount))
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Econ;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
@@ -135,12 +136,12 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public Task BetDraw(ShmartNumber amount, InputValueGuess val, InputColorGuess? col = null)
 | 
			
		||||
        public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputValueGuess val, InputColorGuess? col = null)
 | 
			
		||||
            => BetDrawInternal(amount, val, col);
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public Task BetDraw(ShmartNumber amount, InputColorGuess col, InputValueGuess? val = null)
 | 
			
		||||
        public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputColorGuess col, InputValueGuess? val = null)
 | 
			
		||||
            => BetDrawInternal(amount, val, col);
 | 
			
		||||
        
 | 
			
		||||
        public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
@@ -96,7 +97,7 @@ public partial class Gambling
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task Betflip(ShmartNumber amount, BetFlipGuess guess)
 | 
			
		||||
        public async Task Betflip([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, BetFlipGuess guess)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount) || amount == 1)
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ using System.Collections.Immutable;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using Nadeko.Econ.Gambling.Rps;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling;
 | 
			
		||||
 | 
			
		||||
@@ -428,26 +429,26 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(0)]
 | 
			
		||||
    public async Task Give(ShmartNumber amount, IGuildUser receiver, [Leftover] string msg)
 | 
			
		||||
    public async Task Give([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, IGuildUser receiver, [Leftover] string msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!await _cs.TransferAsync(_eb, ctx.User, receiver, amount, msg, N(amount.Value)))
 | 
			
		||||
        if (!await _cs.TransferAsync(_eb, ctx.User, receiver, amount, msg, N(amount)))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.gifted(N(amount.Value), Format.Bold(receiver.ToString())));
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.gifted(N(amount), Format.Bold(receiver.ToString())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
    public Task Give(ShmartNumber amount, [Leftover] IGuildUser receiver)
 | 
			
		||||
    public Task Give([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, [Leftover] IGuildUser receiver)
 | 
			
		||||
        => Give(amount, receiver, null);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
@@ -583,7 +584,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    public async Task RollDuel(ShmartNumber amount, IUser u)
 | 
			
		||||
    public async Task RollDuel([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, IUser u)
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx.User.Id == u.Id)
 | 
			
		||||
        {
 | 
			
		||||
@@ -622,7 +623,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.roll_duel_challenge(Format.Bold(ctx.User.ToString()),
 | 
			
		||||
                Format.Bold(u.ToString()),
 | 
			
		||||
                Format.Bold(N(amount.Value))));
 | 
			
		||||
                Format.Bold(N(amount))));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        async Task GameOnGameTick(RollDuelGame arg)
 | 
			
		||||
@@ -674,7 +675,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task BetRoll(ShmartNumber amount)
 | 
			
		||||
    public async Task BetRoll([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await CheckBetMandatory(amount))
 | 
			
		||||
        {
 | 
			
		||||
@@ -804,7 +805,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task Rps(InputRpsPick pick, ShmartNumber amount = default)
 | 
			
		||||
    public async Task Rps(InputRpsPick pick, [OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default)
 | 
			
		||||
    {
 | 
			
		||||
        static string GetRpsPick(InputRpsPick p)
 | 
			
		||||
        {
 | 
			
		||||
@@ -840,7 +841,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        else if (result.Result == RpsResultType.Win)
 | 
			
		||||
        {
 | 
			
		||||
            if ((long)result.Won > 0)
 | 
			
		||||
                embed.AddField(GetText(strs.won), N(amount.Value));
 | 
			
		||||
                embed.AddField(GetText(strs.won), N(amount));
 | 
			
		||||
 | 
			
		||||
            msg = GetText(strs.rps_win(ctx.User.Mention,
 | 
			
		||||
                GetRpsPick(pick),
 | 
			
		||||
@@ -864,7 +865,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task LuckyLadder(ShmartNumber amount)
 | 
			
		||||
    public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await CheckBetMandatory(amount))
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +45,7 @@ public partial class Gambling
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async Task Plant(ShmartNumber amount, string pass = null)
 | 
			
		||||
        public async Task Plant([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, string pass = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (amount < 1)
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
@@ -19,13 +20,13 @@ public partial class Gambling
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public Task RaffleCur(Mixed _, ShmartNumber amount)
 | 
			
		||||
        public Task RaffleCur(Mixed _, [OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
            => RaffleCur(amount, true);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public async Task RaffleCur(ShmartNumber amount, bool mixed = false)
 | 
			
		||||
        public async Task RaffleCur([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, bool mixed = false)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount))
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using Nadeko.Econ.Gambling;
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +49,7 @@ public partial class Gambling
 | 
			
		||||
            => Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task Slot(ShmartNumber amount)
 | 
			
		||||
        public async Task Slot([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount))
 | 
			
		||||
                return;
 | 
			
		||||
@@ -76,7 +77,7 @@ public partial class Gambling
 | 
			
		||||
                .WithOkColor();
 | 
			
		||||
 | 
			
		||||
            var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again");
 | 
			
		||||
            var si = new SimpleInteraction<ShmartNumber>(bb, (_, amount) => Slot(amount), amount);
 | 
			
		||||
            var si = new SimpleInteraction<long>(bb, (_, amount) => Slot(amount), amount);
 | 
			
		||||
 | 
			
		||||
            var inter = _inter.Create(ctx.User.Id, si);
 | 
			
		||||
            var msg = await ctx.Channel.SendFileAsync(imgStream,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
public interface IMedusaeRepositoryService
 | 
			
		||||
{
 | 
			
		||||
    Task<List<ModuleItem>> GetModuleItemsAsync();
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,13 @@ namespace NadekoBot.Modules;
 | 
			
		||||
[OwnerOnly]
 | 
			
		||||
public partial class Medusa : NadekoModule<IMedusaLoaderService>
 | 
			
		||||
{
 | 
			
		||||
    private readonly IMedusaeRepositoryService _repo;
 | 
			
		||||
 | 
			
		||||
    public Medusa(IMedusaeRepositoryService repo)
 | 
			
		||||
    {
 | 
			
		||||
        _repo = repo;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async Task MedusaLoad(string? name = null)
 | 
			
		||||
@@ -190,13 +197,34 @@ public partial class Medusa : NadekoModule<IMedusaLoaderService>
 | 
			
		||||
                foreach (var medusa in medusae.Skip(page * 9).Take(9))
 | 
			
		||||
                {
 | 
			
		||||
                    eb.AddField(medusa.Name,
 | 
			
		||||
                        $@"`Sneks:` {medusa.Sneks.Count}
 | 
			
		||||
`Commands:` {medusa.Sneks.Sum(x => x.Commands.Count)}
 | 
			
		||||
--
 | 
			
		||||
{medusa.Description}");
 | 
			
		||||
                        $"""
 | 
			
		||||
                            `Sneks:` {medusa.Sneks.Count}
 | 
			
		||||
                            `Commands:` {medusa.Sneks.Sum(x => x.Commands.Count)}
 | 
			
		||||
                            --
 | 
			
		||||
                            {medusa.Description}
 | 
			
		||||
                            """);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return eb;
 | 
			
		||||
            }, medusae.Count, 9);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async Task MedusaSearch()
 | 
			
		||||
    {
 | 
			
		||||
        var eb = _eb.Create()
 | 
			
		||||
                    .WithTitle(GetText(strs.list_of_medusae))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
        
 | 
			
		||||
        foreach (var item in await _repo.GetModuleItemsAsync())
 | 
			
		||||
        {
 | 
			
		||||
            eb.AddField(item.Name, $"""
 | 
			
		||||
                {item.Description}
 | 
			
		||||
                `{item.Command}`
 | 
			
		||||
                """, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/NadekoBot/Modules/Medusae/MedusaItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/NadekoBot/Modules/Medusae/MedusaItem.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
public class ModuleItem
 | 
			
		||||
{
 | 
			
		||||
    public string Name { get; init; }
 | 
			
		||||
    public string Description { get; init; }
 | 
			
		||||
    public string Command { get; init; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/NadekoBot/Modules/Medusae/MedusaeRepositoryService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/NadekoBot/Modules/Medusae/MedusaeRepositoryService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
public class MedusaeRepositoryService : IMedusaeRepositoryService, INService
 | 
			
		||||
{
 | 
			
		||||
    public async Task<List<ModuleItem>> GetModuleItemsAsync()
 | 
			
		||||
    {
 | 
			
		||||
        // Simulate retrieving data from a database or API
 | 
			
		||||
        await Task.Delay(100);
 | 
			
		||||
        return new List<ModuleItem>
 | 
			
		||||
        {
 | 
			
		||||
            new ModuleItem { Name = "RSS Reader", Description = "Keep up to date with your favorite websites", Command = ".meinstall rss" },
 | 
			
		||||
            new ModuleItem { Name = "Password Manager", Description = "Safely store and manage all your passwords", Command = ".meinstall passwordmanager" },
 | 
			
		||||
            new ModuleItem { Name = "Browser Extension", Description = "Enhance your browsing experience with useful tools", Command = ".meinstall browserextension" },
 | 
			
		||||
            new ModuleItem { Name = "Video Downloader", Description = "Download videos from popular websites", Command = ".meinstall videodownloader" },
 | 
			
		||||
            new ModuleItem { Name = "Virtual Private Network", Description = "Securely browse the web and protect your privacy", Command = ".meinstall vpn" },
 | 
			
		||||
            new ModuleItem { Name = "Ad Blocker", Description = "Block annoying ads and improve page load times", Command = ".meinstall adblocker" },
 | 
			
		||||
            new ModuleItem { Name = "Cloud Storage", Description = "Store and share your files online", Command = ".meinstall cloudstorage" },
 | 
			
		||||
            new ModuleItem { Name = "Social Media Manager", Description = "Manage all your social media accounts in one place", Command = ".meinstall socialmediamanager" },
 | 
			
		||||
            new ModuleItem { Name = "Code Editor", Description = "Write and edit code online", Command = ".meinstall codeeditor" }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/NadekoBot/Modules/Medusae/ModuleItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/NadekoBot/Modules/Medusae/ModuleItem.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
public class ModuleItem
 | 
			
		||||
{
 | 
			
		||||
    public string Name { get; init; }
 | 
			
		||||
    public string Description { get; init; }
 | 
			
		||||
    public string Command { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
public interface ISearchImagesService
 | 
			
		||||
{
 | 
			
		||||
    ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; }
 | 
			
		||||
    ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; }
 | 
			
		||||
    ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; }
 | 
			
		||||
    Task<UrlReply> Gelbooru(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Danbooru(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Konachan(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Yandere(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Rule34(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> E621(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Sankaku(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> SafeBooru(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Hentai(ulong? guildId, bool forceExplicit, string[] tags);
 | 
			
		||||
    Task<UrlReply> Boobs();
 | 
			
		||||
    ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag);
 | 
			
		||||
    ValueTask<string[]> GetBlacklistedTags(ulong guildId);
 | 
			
		||||
    Task<UrlReply> Butts();
 | 
			
		||||
    // Task<Gallery> GetNhentaiByIdAsync(uint id);
 | 
			
		||||
    // Task<Gallery> GetNhentaiBySearchAsync(string search);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
//
 | 
			
		||||
// public interface INhentaiService
 | 
			
		||||
// {
 | 
			
		||||
//     Task<Gallery?> GetAsync(uint id);
 | 
			
		||||
//     Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search);
 | 
			
		||||
// }
 | 
			
		||||
@@ -1,115 +0,0 @@
 | 
			
		||||
// using AngleSharp.Html.Dom;
 | 
			
		||||
// using AngleSharp.Html.Parser;
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class NhentaiScraperService : INhentaiService, INService
 | 
			
		||||
// {
 | 
			
		||||
//     private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
//
 | 
			
		||||
//     private static readonly HtmlParser _htmlParser = new(new()
 | 
			
		||||
//     {
 | 
			
		||||
//         IsScripting = false,
 | 
			
		||||
//         IsEmbedded = false,
 | 
			
		||||
//         IsSupportingProcessingInstructions = false,
 | 
			
		||||
//         IsKeepingSourceReferences = false,
 | 
			
		||||
//         IsNotSupportingFrames = true
 | 
			
		||||
//     });
 | 
			
		||||
//
 | 
			
		||||
//     public NhentaiScraperService(IHttpClientFactory httpFactory)
 | 
			
		||||
//     {
 | 
			
		||||
//         _httpFactory = httpFactory;
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     private HttpClient GetHttpClient()
 | 
			
		||||
//     {
 | 
			
		||||
//         var http = _httpFactory.CreateClient();
 | 
			
		||||
//         http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36");
 | 
			
		||||
//         http.DefaultRequestHeaders.Add("Cookie", "cf_clearance=I5pR71P4wJkRBFTLFjBndI.GwfKwT.Gx06uS8XNmRJo-1657214595-0-150; csrftoken=WMWRLtsQtBVQYvYkbqXKJHI9T1JwWCdd3tNhoxHn7aHLUYHAqe60XFUKAoWsJtda");
 | 
			
		||||
//         return http;
 | 
			
		||||
//     }
 | 
			
		||||
//     
 | 
			
		||||
//     public async Task<Gallery?> GetAsync(uint id)
 | 
			
		||||
//     {
 | 
			
		||||
//         using var http = GetHttpClient();
 | 
			
		||||
//         try
 | 
			
		||||
//         {
 | 
			
		||||
//             var url = $"https://nhentai.net/g/{id}/";
 | 
			
		||||
//             var strRes = await http.GetStringAsync(url);
 | 
			
		||||
//             var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
//
 | 
			
		||||
//             var title = doc.QuerySelector("#info .title")?.TextContent;
 | 
			
		||||
//             var fullTitle = doc.QuerySelector("meta[itemprop=\"name\"]")?.Attributes["content"]?.Value
 | 
			
		||||
//                             ?? title;
 | 
			
		||||
//             var thumb = (doc.QuerySelector("#cover a img") as IHtmlImageElement)?.Dataset["src"];
 | 
			
		||||
//
 | 
			
		||||
//             var tagsElem = doc.QuerySelector("#tags");
 | 
			
		||||
//             
 | 
			
		||||
//             var pageCount = tagsElem?.QuerySelector("a.tag[href^=\"/search/?q=pages\"] span")?.TextContent;
 | 
			
		||||
//             var likes = doc.QuerySelector(".buttons .btn-disabled.btn.tooltip span span")?.TextContent?.Trim('(', ')');
 | 
			
		||||
//             var uploadedAt = (tagsElem?.QuerySelector(".tag-container .tags time.nobold") as IHtmlTimeElement)?.DateTime;
 | 
			
		||||
//
 | 
			
		||||
//             var tags = tagsElem?.QuerySelectorAll(".tag-container .tags > a.tag[href^=\"/tag\"]")
 | 
			
		||||
//                 .Cast<IHtmlAnchorElement>()
 | 
			
		||||
//                 .Select(x => new Tag()
 | 
			
		||||
//                 {
 | 
			
		||||
//                     Name = x.QuerySelector("span:first-child")?.TextContent,
 | 
			
		||||
//                     Url = $"https://nhentai.net{x.PathName}"
 | 
			
		||||
//                 })
 | 
			
		||||
//                 .ToArray();
 | 
			
		||||
//
 | 
			
		||||
//             if (string.IsNullOrWhiteSpace(fullTitle))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!int.TryParse(pageCount, out var pc))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!int.TryParse(likes, out var lc))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             if (!DateTime.TryParse(uploadedAt, out var ua))
 | 
			
		||||
//                 return null;
 | 
			
		||||
//
 | 
			
		||||
//             return new Gallery(id,
 | 
			
		||||
//                 url,
 | 
			
		||||
//                 fullTitle,
 | 
			
		||||
//                 title,
 | 
			
		||||
//                 thumb,
 | 
			
		||||
//                 pc,
 | 
			
		||||
//                 lc,
 | 
			
		||||
//                 ua,
 | 
			
		||||
//                 tags);
 | 
			
		||||
//         }
 | 
			
		||||
//         catch (HttpRequestException)
 | 
			
		||||
//         {
 | 
			
		||||
//             Log.Warning("Nhentai with id {NhentaiId} not found", id);
 | 
			
		||||
//             return null;
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     public async Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search)
 | 
			
		||||
//     {
 | 
			
		||||
//         using var http = GetHttpClient();
 | 
			
		||||
//         try
 | 
			
		||||
//         {
 | 
			
		||||
//             var url = $"https://nhentai.net/search/?q={Uri.EscapeDataString(search)}&sort=popular-today";
 | 
			
		||||
//             var strRes = await http.GetStringAsync(url);
 | 
			
		||||
//             var doc = await _htmlParser.ParseDocumentAsync(strRes);
 | 
			
		||||
//
 | 
			
		||||
//             var elems = doc.QuerySelectorAll(".container .gallery a")
 | 
			
		||||
//                 .Cast<IHtmlAnchorElement>()
 | 
			
		||||
//                 .Where(x => x.PathName.StartsWith("/g/"))
 | 
			
		||||
//                 .Select(x => x.PathName[3..^1])
 | 
			
		||||
//                 .Select(uint.Parse)
 | 
			
		||||
//                 .ToArray();
 | 
			
		||||
//             
 | 
			
		||||
//             return elems;
 | 
			
		||||
//         }
 | 
			
		||||
//         catch (HttpRequestException)
 | 
			
		||||
//         {
 | 
			
		||||
//             Log.Warning("Nhentai search for {NhentaiSearch} failed", search);
 | 
			
		||||
//             return Array.Empty<uint>();
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
@@ -1,444 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
[NoPublicBot]
 | 
			
		||||
public partial class NSFW : NadekoModule<ISearchImagesService>
 | 
			
		||||
{
 | 
			
		||||
    private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new();
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly NadekoRandom _rng;
 | 
			
		||||
 | 
			
		||||
    public NSFW(IHttpClientFactory factory)
 | 
			
		||||
    {
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
        _rng = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task InternalBoobs()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            using (var http = _httpFactory.CreateClient())
 | 
			
		||||
            {
 | 
			
		||||
                obj = JArray.Parse(
 | 
			
		||||
                    await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}"))[0];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task InternalButts(IMessageChannel channel)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            using (var http = _httpFactory.CreateClient())
 | 
			
		||||
            {
 | 
			
		||||
                obj = JArray.Parse(
 | 
			
		||||
                    await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}"))[0];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    public async Task AutoHentai(int interval = 0, [Leftover] string tags = null)
 | 
			
		||||
    {
 | 
			
		||||
        Timer t;
 | 
			
		||||
 | 
			
		||||
        if (interval == 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.AutoHentaiTimers.TryRemove(ctx.Channel.Id, out t))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.stopped);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (interval < 20)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        t = new(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (tags is null || tags.Length == 0)
 | 
			
		||||
                        await InternalDapiCommand(null, true, _service.Hentai);
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        var groups = tags.Split('|');
 | 
			
		||||
                        var group = groups[_rng.Next(0, groups.Length)];
 | 
			
		||||
                        await InternalDapiCommand(group.Split(' '), true, _service.Hentai);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            null,
 | 
			
		||||
            interval * 1000,
 | 
			
		||||
            interval * 1000);
 | 
			
		||||
 | 
			
		||||
        _service.AutoHentaiTimers.AddOrUpdate(ctx.Channel.Id,
 | 
			
		||||
            t,
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                return t;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        await SendConfirmAsync($"Autohentai started. Interval: {interval}, Tags: {string.Join(", ", tags)}");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    public async Task AutoBoobs(int interval = 0)
 | 
			
		||||
    {
 | 
			
		||||
        Timer t;
 | 
			
		||||
 | 
			
		||||
        if (interval == 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.AutoBoobTimers.TryRemove(ctx.Channel.Id, out t))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            t.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.stopped);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (interval < 20)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        t = new(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await InternalBoobs();
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            null,
 | 
			
		||||
            interval * 1000,
 | 
			
		||||
            interval * 1000);
 | 
			
		||||
 | 
			
		||||
        _service.AutoBoobTimers.AddOrUpdate(ctx.Channel.Id,
 | 
			
		||||
            t,
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                return t;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.started(interval));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    [UserPerm(ChannelPerm.ManageMessages)]
 | 
			
		||||
    public async Task AutoButts(int interval = 0)
 | 
			
		||||
    {
 | 
			
		||||
        Timer t;
 | 
			
		||||
 | 
			
		||||
        if (interval == 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_service.AutoButtTimers.TryRemove(ctx.Channel.Id, out t))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.stopped);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (interval < 20)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        t = new(async _ =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await InternalButts(ctx.Channel);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            null,
 | 
			
		||||
            interval * 1000,
 | 
			
		||||
            interval * 1000);
 | 
			
		||||
 | 
			
		||||
        _service.AutoButtTimers.AddOrUpdate(ctx.Channel.Id,
 | 
			
		||||
            t,
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
                return t;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.started(interval));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Hentai(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, true, _service.Hentai);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public async Task HentaiBomb(params string[] tags)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id))
 | 
			
		||||
            return;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags),
 | 
			
		||||
                _service.Danbooru(ctx.Guild?.Id, true, tags),
 | 
			
		||||
                _service.Konachan(ctx.Guild?.Id, true, tags),
 | 
			
		||||
                _service.Gelbooru(ctx.Guild?.Id, true, tags));
 | 
			
		||||
 | 
			
		||||
            var linksEnum = images.Where(l => l is not null).ToArray();
 | 
			
		||||
            if (!linksEnum.Any())
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url)));
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _hentaiBombBlacklist.TryRemove(ctx.Guild?.Id ?? ctx.User.Id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Yandere(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Yandere);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Konachan(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Konachan);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Sankaku(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Sankaku);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task E621(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.E621);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Rule34(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Rule34);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Danbooru(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Danbooru);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Gelbooru(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.Gelbooru);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Derpibooru(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.DerpiBooru);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public Task Safebooru(params string[] tags)
 | 
			
		||||
        => InternalDapiCommand(tags, false, _service.SafeBooru);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public async Task Boobs()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            using (var http = _httpFactory.CreateClient())
 | 
			
		||||
            {
 | 
			
		||||
                obj = JArray.Parse(
 | 
			
		||||
                    await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}"))[0];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    public async Task Butts()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            using (var http = _httpFactory.CreateClient())
 | 
			
		||||
            {
 | 
			
		||||
                obj = JArray.Parse(
 | 
			
		||||
                    await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}"))[0];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [RequireContext(ContextType.Guild)]
 | 
			
		||||
    [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
    public async Task NsfwTagBlacklist([Leftover] string tag = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(tag))
 | 
			
		||||
        {
 | 
			
		||||
            var blTags = await _service.GetBlacklistedTags(ctx.Guild.Id);
 | 
			
		||||
            await SendConfirmAsync(GetText(strs.blacklisted_tag_list), blTags.Any() ? string.Join(", ", blTags) : "-");
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            tag = tag.Trim().ToLowerInvariant();
 | 
			
		||||
            var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag);
 | 
			
		||||
 | 
			
		||||
            if (added)
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag));
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.blacklisted_tag_remove(tag));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    // [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    // [Priority(1)]
 | 
			
		||||
    // public async Task Nhentai(uint id)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var g = await _service.GetNhentaiByIdAsync(id);
 | 
			
		||||
    //
 | 
			
		||||
    //     if (g is null)
 | 
			
		||||
    //     {
 | 
			
		||||
    //         await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
    //         return;
 | 
			
		||||
    //     }
 | 
			
		||||
    //
 | 
			
		||||
    //     await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // [Cmd]
 | 
			
		||||
    // [RequireContext(ContextType.Guild)]
 | 
			
		||||
    // [RequireNsfw(Group = "nsfw_or_dm")]
 | 
			
		||||
    // [RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
 | 
			
		||||
    // [Priority(0)]
 | 
			
		||||
    // public async Task Nhentai([Leftover] string query)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var g = await _service.GetNhentaiBySearchAsync(query);
 | 
			
		||||
    //
 | 
			
		||||
    //     if (g is null)
 | 
			
		||||
    //     {
 | 
			
		||||
    //         await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
    //         return;
 | 
			
		||||
    //     }
 | 
			
		||||
    //
 | 
			
		||||
    //     await SendNhentaiGalleryInternalAsync(g);
 | 
			
		||||
    // }
 | 
			
		||||
    //
 | 
			
		||||
    // private async Task SendNhentaiGalleryInternalAsync(Gallery g)
 | 
			
		||||
    // {
 | 
			
		||||
    //     var count = 0;
 | 
			
		||||
    //     var tagString = g.Tags.Shuffle()
 | 
			
		||||
    //                      .Select(tag => $"[{tag.Name}]({tag.Url})")
 | 
			
		||||
    //                      .TakeWhile(tag => (count += tag.Length) < 1000)
 | 
			
		||||
    //                      .Join(" ");
 | 
			
		||||
    //
 | 
			
		||||
    //     var embed = _eb.Create()
 | 
			
		||||
    //         .WithTitle(g.Title)
 | 
			
		||||
    //         .WithDescription(g.FullTitle)
 | 
			
		||||
    //         .WithImageUrl(g.Thumbnail)
 | 
			
		||||
    //         .WithUrl(g.Url)
 | 
			
		||||
    //         .AddField(GetText(strs.favorites), g.Likes, true)
 | 
			
		||||
    //         .AddField(GetText(strs.pages), g.PageCount, true)
 | 
			
		||||
    //         .AddField(GetText(strs.tags),
 | 
			
		||||
    //             string.IsNullOrWhiteSpace(tagString)
 | 
			
		||||
    //                 ? "?"
 | 
			
		||||
    //                 : tagString,
 | 
			
		||||
    //             true)
 | 
			
		||||
    //         .WithFooter(g.UploadedAt.ToString("f"))
 | 
			
		||||
    //         .WithOkColor();
 | 
			
		||||
    //
 | 
			
		||||
    //     await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    private async Task InternalDapiCommand(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool forceExplicit,
 | 
			
		||||
        Func<ulong?, bool, string[], Task<UrlReply>> func)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await func(ctx.Guild?.Id, forceExplicit, tags);
 | 
			
		||||
 | 
			
		||||
        if (data is null || !string.IsNullOrWhiteSpace(data.Error))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(_eb.Create(ctx)
 | 
			
		||||
                                        .WithOkColor()
 | 
			
		||||
                                        .WithImageUrl(data.Url)
 | 
			
		||||
                                        .WithDescription($"[link]({data.Url})")
 | 
			
		||||
                                        .WithFooter(
 | 
			
		||||
                                            $"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,320 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class SearchImageCacher : INService
 | 
			
		||||
{
 | 
			
		||||
    private static readonly ISet<string> _defaultTagBlacklist = new HashSet<string>
 | 
			
		||||
    {
 | 
			
		||||
        "loli",
 | 
			
		||||
        "lolicon",
 | 
			
		||||
        "shota",
 | 
			
		||||
        "shotacon",
 | 
			
		||||
        "cub"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
    private readonly Dictionary<Booru, object> _typeLocks = new();
 | 
			
		||||
    private readonly Dictionary<Booru, HashSet<string>> _usedTags = new();
 | 
			
		||||
    private readonly IMemoryCache _cache;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<(Booru, string), int> _maxPages = new();
 | 
			
		||||
 | 
			
		||||
    public SearchImageCacher(IHttpClientFactory httpFactory, IMemoryCache cache)
 | 
			
		||||
    {
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
        _cache = cache;
 | 
			
		||||
 | 
			
		||||
        // initialize new cache with empty values
 | 
			
		||||
        foreach (var type in Enum.GetValues<Booru>())
 | 
			
		||||
        {
 | 
			
		||||
            _typeLocks[type] = new();
 | 
			
		||||
            _usedTags[type] = new();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string Key(Booru boory, string tag)
 | 
			
		||||
        => $"booru:{boory}__tag:{tag}";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Download images of the specified type, and cache them.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tags">Required tags</param>
 | 
			
		||||
    /// <param name="forceExplicit">Whether images will be forced to be explicit</param>
 | 
			
		||||
    /// <param name="type">Provider type</param>
 | 
			
		||||
    /// <param name="cancel">Cancellation token</param>
 | 
			
		||||
    /// <returns>Whether any image is found.</returns>
 | 
			
		||||
    private async Task<bool> UpdateImagesInternalAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool forceExplicit,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        CancellationToken cancel)
 | 
			
		||||
    {
 | 
			
		||||
        var images = await DownloadImagesAsync(tags, forceExplicit, type, cancel);
 | 
			
		||||
        if (images is null || images.Count == 0)
 | 
			
		||||
            // Log.Warning("Got no images for {0}, tags: {1}", type, string.Join(", ", tags));
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        Log.Information("Updating {Type}...", type);
 | 
			
		||||
        lock (_typeLocks[type])
 | 
			
		||||
        {
 | 
			
		||||
            var typeUsedTags = _usedTags[type];
 | 
			
		||||
            foreach (var tag in tags)
 | 
			
		||||
                typeUsedTags.Add(tag);
 | 
			
		||||
 | 
			
		||||
            // if user uses no tags for the hentai command and there are no used
 | 
			
		||||
            // tags atm, just select 50 random tags from downloaded images to seed
 | 
			
		||||
            if (typeUsedTags.Count == 0)
 | 
			
		||||
                images.SelectMany(x => x.Tags).Distinct().Shuffle().Take(50).ToList().ForEach(x => typeUsedTags.Add(x));
 | 
			
		||||
 | 
			
		||||
            foreach (var img in images)
 | 
			
		||||
            {
 | 
			
		||||
                // if any of the tags is a tag banned by discord
 | 
			
		||||
                // do not put that image in the cache
 | 
			
		||||
                if (_defaultTagBlacklist.Overlaps(img.Tags))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                // if image doesn't have a proper absolute uri, skip it
 | 
			
		||||
                if (!Uri.IsWellFormedUriString(img.FileUrl, UriKind.Absolute))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                // i'm appending current tags because of tag aliasing
 | 
			
		||||
                // this way, if user uses tag alias, for example 'kissing' -
 | 
			
		||||
                // both 'kiss' (real tag returned by the image) and 'kissing' will be populated with
 | 
			
		||||
                // retreived images
 | 
			
		||||
                foreach (var tag in img.Tags.Concat(tags).Distinct())
 | 
			
		||||
                {
 | 
			
		||||
                    if (typeUsedTags.Contains(tag))
 | 
			
		||||
                    {
 | 
			
		||||
                        var set = _cache.GetOrCreate<HashSet<ImageData>>(Key(type, tag),
 | 
			
		||||
                            e =>
 | 
			
		||||
                            {
 | 
			
		||||
                                e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
 | 
			
		||||
                                return new();
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                        if (set.Count < 100)
 | 
			
		||||
                            set.Add(img);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ImageData QueryLocal(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        HashSet<string> blacklistedTags)
 | 
			
		||||
    {
 | 
			
		||||
        var setList = new List<HashSet<ImageData>>();
 | 
			
		||||
 | 
			
		||||
        // ofc make sure no changes are happening while we're getting a random one
 | 
			
		||||
        lock (_typeLocks[type])
 | 
			
		||||
        {
 | 
			
		||||
            // if no tags are provided, get a random tag
 | 
			
		||||
            if (tags.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                // get all tags in the cache
 | 
			
		||||
                if (_usedTags.TryGetValue(type, out var allTags) && allTags.Count > 0)
 | 
			
		||||
                    tags = new[] { allTags.ToList()[_rng.Next(0, allTags.Count)] };
 | 
			
		||||
                else
 | 
			
		||||
                    return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var tag in tags)
 | 
			
		||||
                // if any tag is missing from cache, that means there is no result
 | 
			
		||||
            {
 | 
			
		||||
                if (_cache.TryGetValue<HashSet<ImageData>>(Key(type, tag), out var set))
 | 
			
		||||
                    setList.Add(set);
 | 
			
		||||
                else
 | 
			
		||||
                    return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (setList.Count == 0)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            List<ImageData> resultList;
 | 
			
		||||
            // if multiple tags, we need to interesect sets
 | 
			
		||||
            if (setList.Count > 1)
 | 
			
		||||
            {
 | 
			
		||||
                // now that we have sets, interesect them to find eligible items
 | 
			
		||||
                // make a copy of the 1st set
 | 
			
		||||
                var resultSet = new HashSet<ImageData>(setList[0]);
 | 
			
		||||
 | 
			
		||||
                // go through all other sets, and
 | 
			
		||||
                for (var i = 1; i < setList.Count; ++i)
 | 
			
		||||
                    // if any of the elements in result set are not present in the current set
 | 
			
		||||
                    // remove it from the result set
 | 
			
		||||
                    resultSet.IntersectWith(setList[i]);
 | 
			
		||||
 | 
			
		||||
                resultList = resultSet.ToList();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // if only one tag, use that set
 | 
			
		||||
                resultList = setList[0].ToList();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // return a random one which doesn't have blacklisted tags in it
 | 
			
		||||
            resultList = resultList.Where(x => !blacklistedTags.Overlaps(x.Tags)).ToList();
 | 
			
		||||
 | 
			
		||||
            // if no items in the set -> not found
 | 
			
		||||
            if (resultList.Count == 0)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var toReturn = resultList[_rng.Next(0, resultList.Count)];
 | 
			
		||||
 | 
			
		||||
            // remove from cache
 | 
			
		||||
            foreach (var tag in tags)
 | 
			
		||||
            {
 | 
			
		||||
                if (_cache.TryGetValue<HashSet<ImageData>>(Key(type, tag), out var items))
 | 
			
		||||
                    items.Remove(toReturn);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return toReturn;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<ImageData> GetImageNew(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool forceExplicit,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        HashSet<string> blacklistedTags,
 | 
			
		||||
        CancellationToken cancel)
 | 
			
		||||
    {
 | 
			
		||||
        // make sure tags are proper
 | 
			
		||||
        tags = tags.Where(x => x is not null).Select(tag => tag.ToLowerInvariant().Trim()).Distinct().ToArray();
 | 
			
		||||
 | 
			
		||||
        if (tags.Length > 2 && type == Booru.Danbooru)
 | 
			
		||||
            tags = tags[..2];
 | 
			
		||||
 | 
			
		||||
        // use both tags banned by discord and tags banned on the server 
 | 
			
		||||
        if (blacklistedTags.Overlaps(tags) || _defaultTagBlacklist.Overlaps(tags))
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        // query for an image
 | 
			
		||||
        var image = QueryLocal(tags, type, blacklistedTags);
 | 
			
		||||
        if (image is not null)
 | 
			
		||||
            return image;
 | 
			
		||||
 | 
			
		||||
        var success = false;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // if image is not found, update the cache and query again
 | 
			
		||||
            success = await UpdateImagesInternalAsync(tags, forceExplicit, type, cancel);
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpRequestException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!success)
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        image = QueryLocal(tags, type, blacklistedTags);
 | 
			
		||||
 | 
			
		||||
        return image;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<ImageData>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool isExplicit,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        CancellationToken cancel)
 | 
			
		||||
    {
 | 
			
		||||
        var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
 | 
			
		||||
 | 
			
		||||
        var attempt = 0;
 | 
			
		||||
        while (attempt++ <= 10)
 | 
			
		||||
        {
 | 
			
		||||
            int page;
 | 
			
		||||
            if (_maxPages.TryGetValue((type, tagStr), out var maxPage))
 | 
			
		||||
            {
 | 
			
		||||
                if (maxPage == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Information("Tag {Tags} yields no result on {Type}, skipping", tagStr, type);
 | 
			
		||||
                    return new();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                page = _rng.Next(0, maxPage);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
                page = _rng.Next(0, 11);
 | 
			
		||||
 | 
			
		||||
            var result = await DownloadImagesAsync(tags, isExplicit, type, page, cancel);
 | 
			
		||||
 | 
			
		||||
            if (result is null or { Count: 0 })
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Tag {Tags}, page {Page} has no result on {Type}",
 | 
			
		||||
                    string.Join(", ", tags),
 | 
			
		||||
                    page,
 | 
			
		||||
                    type.ToString());
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IImageDownloader GetImageDownloader(Booru booru)
 | 
			
		||||
        => booru switch
 | 
			
		||||
        {
 | 
			
		||||
            Booru.Danbooru => new DanbooruImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Yandere => new YandereImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Konachan => new KonachanImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Safebooru => new SafebooruImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.E621 => new E621ImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Derpibooru => new DerpibooruImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Gelbooru => new GelbooruImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Rule34 => new Rule34ImageDownloader(_httpFactory),
 | 
			
		||||
            Booru.Sankaku => new SankakuImageDownloader(_httpFactory),
 | 
			
		||||
            _ => throw new NotImplementedException($"{booru} downloader not implemented.")
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private async Task<List<ImageData>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool isExplicit,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        int page,
 | 
			
		||||
        CancellationToken cancel)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Downloading from {Type} (page {Page})...", type, page);
 | 
			
		||||
 | 
			
		||||
            var downloader = GetImageDownloader(type);
 | 
			
		||||
 | 
			
		||||
            var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel);
 | 
			
		||||
            if (images.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
 | 
			
		||||
                _maxPages[(type, tagStr)] = page;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return images;
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex,
 | 
			
		||||
                "Error downloading an image:\nTags: {Tags}\nType: {Type}\nPage: {Page}\nMessage: {Message}",
 | 
			
		||||
                string.Join(", ", tags),
 | 
			
		||||
                type,
 | 
			
		||||
                page,
 | 
			
		||||
                ex.Message);
 | 
			
		||||
            return new();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,297 +0,0 @@
 | 
			
		||||
#nullable disable warnings
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
public class SearchImagesService : ISearchImagesService, INService
 | 
			
		||||
{
 | 
			
		||||
    private ConcurrentDictionary<ulong, HashSet<string>> BlacklistedTags { get; }
 | 
			
		||||
 | 
			
		||||
    public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new();
 | 
			
		||||
    public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new();
 | 
			
		||||
    public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new();
 | 
			
		||||
    
 | 
			
		||||
    private readonly Random _rng;
 | 
			
		||||
    private readonly SearchImageCacher _cache;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
    private readonly object _taglock = new();
 | 
			
		||||
 | 
			
		||||
    public SearchImagesService(
 | 
			
		||||
        DbService db,
 | 
			
		||||
        SearchImageCacher cacher,
 | 
			
		||||
        IHttpClientFactory httpFactory
 | 
			
		||||
        )
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
        _cache = cacher;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
 | 
			
		||||
        using var uow = db.GetDbContext();
 | 
			
		||||
        BlacklistedTags = new(uow.NsfwBlacklistedTags.AsEnumerable()
 | 
			
		||||
                                 .GroupBy(x => x.GuildId)
 | 
			
		||||
                                 .ToDictionary(x => x.Key, x => new HashSet<string>(x.Select(y => y.Tag))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task<UrlReply> GetNsfwImageAsync(
 | 
			
		||||
        ulong? guildId,
 | 
			
		||||
        bool forceExplicit,
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        Booru dapi,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
        => GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty<string>(), forceExplicit, dapi, cancel);
 | 
			
		||||
 | 
			
		||||
    private bool IsValidTag(string tag)
 | 
			
		||||
        => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or /
 | 
			
		||||
 | 
			
		||||
    private async Task<UrlReply> GetNsfwImageAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        bool forceExplicit,
 | 
			
		||||
        Booru dapi,
 | 
			
		||||
        CancellationToken cancel)
 | 
			
		||||
    {
 | 
			
		||||
        if (!tags.All(x => IsValidTag(x)))
 | 
			
		||||
        {
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = "One or more tags are invalid.",
 | 
			
		||||
                Url = ""
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            BlacklistedTags.TryGetValue(guildId, out var blTags);
 | 
			
		||||
 | 
			
		||||
            if (dapi == Booru.E621)
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 0; i < tags.Length; ++i)
 | 
			
		||||
                {
 | 
			
		||||
                    if (tags[i] == "yuri")
 | 
			
		||||
                        tags[i] = "female/female";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (dapi == Booru.Derpibooru)
 | 
			
		||||
            {
 | 
			
		||||
                for (var i = 0; i < tags.Length; ++i)
 | 
			
		||||
                {
 | 
			
		||||
                    if (tags[i] == "yuri")
 | 
			
		||||
                        tags[i] = "lesbian";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet<string>(), cancel);
 | 
			
		||||
 | 
			
		||||
            if (result is null)
 | 
			
		||||
            {
 | 
			
		||||
                return new()
 | 
			
		||||
                {
 | 
			
		||||
                    Error = "Image not found.",
 | 
			
		||||
                    Url = ""
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var reply = new UrlReply
 | 
			
		||||
            {
 | 
			
		||||
                Error = "",
 | 
			
		||||
                Url = result.FileUrl,
 | 
			
		||||
                Rating = result.Rating,
 | 
			
		||||
                Provider = result.SearchType.ToString()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            reply.Tags.AddRange(result.Tags);
 | 
			
		||||
 | 
			
		||||
            return reply;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message);
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = ex.Message,
 | 
			
		||||
                Url = ""
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Gelbooru(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Danbooru(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Konachan(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Yandere(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Rule34(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> E621(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> SafeBooru(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru);
 | 
			
		||||
 | 
			
		||||
    public Task<UrlReply> Sankaku(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
        => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku);
 | 
			
		||||
 | 
			
		||||
    public async Task<UrlReply> Hentai(ulong? guildId, bool forceExplicit, string[] tags)
 | 
			
		||||
    {
 | 
			
		||||
        var providers = new[] { Booru.Danbooru, Booru.Konachan, Booru.Gelbooru, Booru.Yandere };
 | 
			
		||||
 | 
			
		||||
        using var cancelSource = new CancellationTokenSource();
 | 
			
		||||
 | 
			
		||||
        // create a task for each type
 | 
			
		||||
        var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList();
 | 
			
		||||
        do
 | 
			
		||||
        {
 | 
			
		||||
            // wait for any of the tasks to complete
 | 
			
		||||
            var task = await Task.WhenAny(tasks);
 | 
			
		||||
 | 
			
		||||
            // get its result
 | 
			
		||||
            var result = task.GetAwaiter().GetResult();
 | 
			
		||||
            if (result.Error == "")
 | 
			
		||||
            {
 | 
			
		||||
                // if we have a non-error result, cancel other searches and return the result
 | 
			
		||||
                cancelSource.Cancel();
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if the result is an error, remove that task from the waiting list,
 | 
			
		||||
            // and wait for another task to complete
 | 
			
		||||
            tasks.Remove(task);
 | 
			
		||||
        } while (tasks.Count > 0); // keep looping as long as there is any task remaining to be attempted
 | 
			
		||||
 | 
			
		||||
        // if we ran out of tasks, that means all tasks failed - return an error
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Error = "No hentai image found."
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<UrlReply> Boobs()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            http.AddFakeHeaders();
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}"))[0];
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = "",
 | 
			
		||||
                Url = $"http://media.oboobs.ru/{obj["preview"]}"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message);
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = ex.Message,
 | 
			
		||||
                Url = ""
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_taglock)
 | 
			
		||||
        {
 | 
			
		||||
            tag = tag.Trim().ToLowerInvariant();
 | 
			
		||||
            var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet<string>());
 | 
			
		||||
            var isAdded = blacklistedTags.Add(tag);
 | 
			
		||||
 | 
			
		||||
            using var uow = _db.GetDbContext();
 | 
			
		||||
            if (!isAdded)
 | 
			
		||||
            {
 | 
			
		||||
                blacklistedTags.Remove(tag);
 | 
			
		||||
                uow.NsfwBlacklistedTags.DeleteAsync(x => x.GuildId == guildId && x.Tag == tag);
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                uow.NsfwBlacklistedTags.Add(new()
 | 
			
		||||
                {
 | 
			
		||||
                    Tag = tag,
 | 
			
		||||
                    GuildId = guildId
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                uow.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new(isAdded);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<string[]> GetBlacklistedTags(ulong guildId)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_taglock)
 | 
			
		||||
        {
 | 
			
		||||
            if (BlacklistedTags.TryGetValue(guildId, out var tags))
 | 
			
		||||
                return new(tags.ToArray());
 | 
			
		||||
 | 
			
		||||
            return new(Array.Empty<string>());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<UrlReply> Butts()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var http = _httpFactory.CreateClient();
 | 
			
		||||
            http.AddFakeHeaders();
 | 
			
		||||
            JToken obj;
 | 
			
		||||
            obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0];
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = "",
 | 
			
		||||
                Url = $"http://media.obutts.ru/{obj["preview"]}"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message);
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Error = ex.Message,
 | 
			
		||||
                Url = ""
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    #region Nhentai
 | 
			
		||||
 | 
			
		||||
    public Task<Gallery?> GetNhentaiByIdAsync(uint id)
 | 
			
		||||
        => _nh.GetAsync(id);
 | 
			
		||||
 | 
			
		||||
    public async Task<Gallery?> GetNhentaiBySearchAsync(string search)
 | 
			
		||||
    {
 | 
			
		||||
        var ids = await _nh.GetIdsBySearchAsync(search);
 | 
			
		||||
 | 
			
		||||
        if (ids.Count == 0)
 | 
			
		||||
            return null;
 | 
			
		||||
        
 | 
			
		||||
        var id = ids[_rng.Next(0, ids.Count)];
 | 
			
		||||
        return await _nh.GetAsync(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    */
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#nullable disable warnings
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw;
 | 
			
		||||
 | 
			
		||||
public record UrlReply
 | 
			
		||||
{
 | 
			
		||||
    public string Error { get; init; }
 | 
			
		||||
    public string Url { get; init; }
 | 
			
		||||
    public string Rating { get; init; }
 | 
			
		||||
    public string Provider { get; init; }
 | 
			
		||||
    public List<string> Tags { get; } = new();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public enum Booru
 | 
			
		||||
{
 | 
			
		||||
    Safebooru,
 | 
			
		||||
    E621,
 | 
			
		||||
    Derpibooru,
 | 
			
		||||
    Rule34,
 | 
			
		||||
    Gelbooru,
 | 
			
		||||
    Konachan,
 | 
			
		||||
    Yandere,
 | 
			
		||||
    Danbooru,
 | 
			
		||||
    Sankaku
 | 
			
		||||
}
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class DapiImageObject : IImageData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("File_Url")]
 | 
			
		||||
    public string FileUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Tags { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("Tag_String")]
 | 
			
		||||
    public string TagString { get; set; }
 | 
			
		||||
 | 
			
		||||
    public int Score { get; set; }
 | 
			
		||||
    public string Rating { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(FileUrl, type, Tags?.Split(' ') ?? TagString?.Split(' '), Score.ToString() ?? Rating);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public readonly struct DapiTag
 | 
			
		||||
{
 | 
			
		||||
    public string Name { get; }
 | 
			
		||||
 | 
			
		||||
    [JsonConstructor]
 | 
			
		||||
    public DapiTag(string name)
 | 
			
		||||
        => Name = name;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class DerpiContainer
 | 
			
		||||
{
 | 
			
		||||
    public DerpiImageObject[] Images { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DerpiImageObject : IImageData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("view_url")]
 | 
			
		||||
    public string ViewUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string[] Tags { get; set; }
 | 
			
		||||
    public int Score { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(ViewUrl, type, Tags, Score.ToString("F1"));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class DanbooruImageDownloader : DapiImageDownloader
 | 
			
		||||
{
 | 
			
		||||
    // using them as concurrent hashsets, value doesn't matter
 | 
			
		||||
    private static readonly ConcurrentDictionary<string, bool> _existentTags = new();
 | 
			
		||||
    private static readonly ConcurrentDictionary<string, bool> _nonexistentTags = new();
 | 
			
		||||
 | 
			
		||||
    public DanbooruImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Danbooru, http, "http://danbooru.donmai.us")
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<bool> IsTagValid(string tag, CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        if (_existentTags.ContainsKey(tag))
 | 
			
		||||
            return true;
 | 
			
		||||
 | 
			
		||||
        if (_nonexistentTags.ContainsKey(tag))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var tags = await http.GetFromJsonAsync<DapiTag[]>(
 | 
			
		||||
            _baseUrl + "/tags.json" + $"?search[name_or_alias_matches]={tag}",
 | 
			
		||||
            _serializerOptions,
 | 
			
		||||
            cancel);
 | 
			
		||||
        if (tags is { Length: > 0 })
 | 
			
		||||
            return _existentTags[tag] = true;
 | 
			
		||||
 | 
			
		||||
        return _nonexistentTags[tag] = false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
 | 
			
		||||
{
 | 
			
		||||
    protected readonly string _baseUrl;
 | 
			
		||||
 | 
			
		||||
    public DapiImageDownloader(Booru booru, IHttpClientFactory http, string baseUrl)
 | 
			
		||||
        : base(booru, http)
 | 
			
		||||
        => _baseUrl = baseUrl;
 | 
			
		||||
 | 
			
		||||
    public abstract Task<bool> IsTagValid(string tag, CancellationToken cancel = default);
 | 
			
		||||
 | 
			
		||||
    protected async Task<bool> AllTagsValid(string[] tags, CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var results = await tags.Select(tag => IsTagValid(tag, cancel)).WhenAll();
 | 
			
		||||
 | 
			
		||||
        // if any of the tags is not valid, the query is not valid
 | 
			
		||||
        foreach (var result in results)
 | 
			
		||||
        {
 | 
			
		||||
            if (!result)
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<DapiImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        // up to 2 tags allowed on danbooru
 | 
			
		||||
        if (tags.Length > 2)
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        if (!await AllTagsValid(tags, cancel))
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
 | 
			
		||||
        var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}";
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
 | 
			
		||||
        if (imageObjects is null)
 | 
			
		||||
            return new();
 | 
			
		||||
        return imageObjects.Where(x => x.FileUrl is not null).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
 | 
			
		||||
{
 | 
			
		||||
    public DerpibooruImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Derpibooru, http)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<DerpiImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
        var uri =
 | 
			
		||||
            $"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}";
 | 
			
		||||
        using var req = new HttpRequestMessage(HttpMethod.Get, uri);
 | 
			
		||||
        req.Headers.AddFakeHeaders();
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        using var res = await http.SendAsync(req, cancel);
 | 
			
		||||
        res.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
        var container = await res.Content.ReadFromJsonAsync<DerpiContainer>(_serializerOptions, cancel);
 | 
			
		||||
        if (container?.Images is null)
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return container.Images.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class E621ImageDownloader : ImageDownloader<E621Object>
 | 
			
		||||
{
 | 
			
		||||
    public E621ImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.E621, http)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<E621Object>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
        var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}";
 | 
			
		||||
        using var req = new HttpRequestMessage(HttpMethod.Get, uri);
 | 
			
		||||
        req.Headers.AddFakeHeaders();
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        using var res = await http.SendAsync(req, cancel);
 | 
			
		||||
        res.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
        var data = await res.Content.ReadFromJsonAsync<E621Response>(_serializerOptions, cancel);
 | 
			
		||||
        if (data?.Posts is null)
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return data.Posts.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class E621Response
 | 
			
		||||
{
 | 
			
		||||
    public List<E621Object> Posts { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
 | 
			
		||||
{
 | 
			
		||||
    public GelbooruImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Gelbooru, http)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<DapiImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
        var uri = $"https://gelbooru.com/index.php?page=dapi"
 | 
			
		||||
                  + $"&s=post"
 | 
			
		||||
                  + $"&json=1"
 | 
			
		||||
                  + $"&q=index"
 | 
			
		||||
                  + $"&limit=100"
 | 
			
		||||
                  + $"&tags={tagString}"
 | 
			
		||||
                  + $"&pid={page}";
 | 
			
		||||
        using var req = new HttpRequestMessage(HttpMethod.Get, uri);
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        using var res = await http.SendAsync(req, cancel);
 | 
			
		||||
        res.EnsureSuccessStatusCode();
 | 
			
		||||
        var resString = await res.Content.ReadAsStringAsync(cancel);
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(resString))
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        var images = JsonSerializer.Deserialize<GelbooruResponse>(resString, _serializerOptions);
 | 
			
		||||
        if (images is null or { Post: null })
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return images.Post.Where(x => x.FileUrl is not null).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class GelbooruResponse
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("post")]
 | 
			
		||||
    public List<DapiImageObject> Post { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public interface IImageDownloader
 | 
			
		||||
{
 | 
			
		||||
    Task<List<ImageData>> DownloadImageDataAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page = 0,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public abstract class ImageDownloader<T> : IImageDownloader
 | 
			
		||||
    where T : IImageData
 | 
			
		||||
{
 | 
			
		||||
    public Booru Booru { get; }
 | 
			
		||||
    protected readonly IHttpClientFactory _http;
 | 
			
		||||
 | 
			
		||||
    protected readonly JsonSerializerOptions _serializerOptions = new()
 | 
			
		||||
    {
 | 
			
		||||
        PropertyNameCaseInsensitive = true,
 | 
			
		||||
        NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public ImageDownloader(Booru booru, IHttpClientFactory http)
 | 
			
		||||
    {
 | 
			
		||||
        _http = http;
 | 
			
		||||
        Booru = booru;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract Task<List<T>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default);
 | 
			
		||||
 | 
			
		||||
    public async Task<List<ImageData>> DownloadImageDataAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var images = await DownloadImagesAsync(tags, page, isExplicit, cancel);
 | 
			
		||||
        return images.Select(x => x.ToCachedImageData(Booru)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public static class ImageDownloaderHelper
 | 
			
		||||
{
 | 
			
		||||
    public static string GetTagString(IEnumerable<string> tags, bool isExplicit = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (isExplicit)
 | 
			
		||||
            tags = tags.Append("rating:explicit");
 | 
			
		||||
 | 
			
		||||
        return string.Join('+', tags.Select(x => x.ToLowerInvariant()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
 | 
			
		||||
{
 | 
			
		||||
    private readonly string _baseUrl;
 | 
			
		||||
 | 
			
		||||
    public KonachanImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Konachan, http)
 | 
			
		||||
        => _baseUrl = "https://konachan.com";
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<DapiImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
        var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}";
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
 | 
			
		||||
        if (imageObjects is null)
 | 
			
		||||
            return new();
 | 
			
		||||
        return imageObjects.Where(x => x.FileUrl is not null).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
 | 
			
		||||
{
 | 
			
		||||
    public Rule34ImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Rule34, http)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<Rule34Object>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags);
 | 
			
		||||
        var uri = $"https://api.rule34.xxx//index.php?page=dapi&s=post"
 | 
			
		||||
                  + $"&q=index"
 | 
			
		||||
                  + $"&json=1"
 | 
			
		||||
                  + $"&limit=100"
 | 
			
		||||
                  + $"&tags={tagString}"
 | 
			
		||||
                  + $"&pid={page}";
 | 
			
		||||
        
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders
 | 
			
		||||
            .TryAddWithoutValidation("cookie", "cf_clearance=Gg3bVffg9fOL_.9fIdKmu5PJS86eTI.yTrhbR8z2tPc-1652310659-0-250");
 | 
			
		||||
 | 
			
		||||
        http.DefaultRequestHeaders
 | 
			
		||||
            .TryAddWithoutValidation("user-agent",
 | 
			
		||||
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36");
 | 
			
		||||
        var images = await http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel);
 | 
			
		||||
 | 
			
		||||
        if (images is null)
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return images.Where(img => !string.IsNullOrWhiteSpace(img.Image)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
 | 
			
		||||
{
 | 
			
		||||
    public SafebooruImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Safebooru, http)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<SafebooruElement>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags);
 | 
			
		||||
        var uri =
 | 
			
		||||
            $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}";
 | 
			
		||||
        
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var images = await http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancel);
 | 
			
		||||
        if (images is null)
 | 
			
		||||
            return new();
 | 
			
		||||
 | 
			
		||||
        return images;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
 | 
			
		||||
{
 | 
			
		||||
    private readonly string _baseUrl;
 | 
			
		||||
 | 
			
		||||
    public SankakuImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Sankaku, http)
 | 
			
		||||
    {
 | 
			
		||||
        _baseUrl = "https://capi-v2.sankakucomplex.com";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<SankakuImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        // explicit probably not supported
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags);
 | 
			
		||||
 | 
			
		||||
        var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50";
 | 
			
		||||
        
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        http.AddFakeHeaders();
 | 
			
		||||
        var data = await http.GetStringAsync(uri, cancel);
 | 
			
		||||
        return JsonSerializer.Deserialize<SankakuImageObject[]>(data, _serializerOptions)
 | 
			
		||||
                             ?.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
 | 
			
		||||
                             .ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
 | 
			
		||||
{
 | 
			
		||||
    private readonly string _baseUrl;
 | 
			
		||||
 | 
			
		||||
    public YandereImageDownloader(IHttpClientFactory http)
 | 
			
		||||
        : base(Booru.Yandere, http)
 | 
			
		||||
        => _baseUrl = "https://yande.re";
 | 
			
		||||
 | 
			
		||||
    public override async Task<List<DapiImageObject>> DownloadImagesAsync(
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        int page,
 | 
			
		||||
        bool isExplicit = false,
 | 
			
		||||
        CancellationToken cancel = default)
 | 
			
		||||
    {
 | 
			
		||||
        var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
 | 
			
		||||
 | 
			
		||||
        var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}";
 | 
			
		||||
        
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
 | 
			
		||||
        if (imageObjects is null)
 | 
			
		||||
            return new();
 | 
			
		||||
        return imageObjects.Where(x => x.FileUrl is not null).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class E621Object : IImageData
 | 
			
		||||
{
 | 
			
		||||
    public FileData File { get; set; }
 | 
			
		||||
    public TagData Tags { get; set; }
 | 
			
		||||
    public ScoreData Score { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(File.Url, Booru.E621, Tags.General, Score.Total.ToString());
 | 
			
		||||
 | 
			
		||||
    public class FileData
 | 
			
		||||
    {
 | 
			
		||||
        public string Url { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class TagData
 | 
			
		||||
    {
 | 
			
		||||
        public string[] General { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class ScoreData
 | 
			
		||||
    {
 | 
			
		||||
        public int Total { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public interface IImageData
 | 
			
		||||
{
 | 
			
		||||
    ImageData ToCachedImageData(Booru type);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class ImageData : IComparable<ImageData>
 | 
			
		||||
{
 | 
			
		||||
    public Booru SearchType { get; }
 | 
			
		||||
    public string FileUrl { get; }
 | 
			
		||||
    public HashSet<string> Tags { get; }
 | 
			
		||||
    public string Rating { get; }
 | 
			
		||||
 | 
			
		||||
    public ImageData(
 | 
			
		||||
        string url,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        string[] tags,
 | 
			
		||||
        string rating)
 | 
			
		||||
    {
 | 
			
		||||
        if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute))
 | 
			
		||||
            FileUrl = "https://danbooru.donmai.us" + url;
 | 
			
		||||
        else
 | 
			
		||||
            FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url;
 | 
			
		||||
 | 
			
		||||
        SearchType = type;
 | 
			
		||||
        FileUrl = url;
 | 
			
		||||
        Tags = tags.ToHashSet();
 | 
			
		||||
        Rating = rating;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
        => FileUrl;
 | 
			
		||||
 | 
			
		||||
    public override int GetHashCode()
 | 
			
		||||
        => FileUrl.GetHashCode();
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object obj)
 | 
			
		||||
        => obj is ImageData ico && ico.FileUrl == FileUrl;
 | 
			
		||||
 | 
			
		||||
    public int CompareTo(ImageData other)
 | 
			
		||||
        => string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class Rule34Object : IImageData
 | 
			
		||||
{
 | 
			
		||||
    public string Image { get; init; }
 | 
			
		||||
    public int Directory { get; init; }
 | 
			
		||||
    public string Tags { get; init; }
 | 
			
		||||
    public int Score { get; init; }
 | 
			
		||||
    [JsonPropertyName("file_url")]
 | 
			
		||||
    public string FileUrl { get; init; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(FileUrl, Booru.Rule34, Tags.Split(' '), Score.ToString());
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class SafebooruElement : IImageData
 | 
			
		||||
{
 | 
			
		||||
    public string Directory { get; set; }
 | 
			
		||||
    public string Image { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public string FileUrl
 | 
			
		||||
        => $"https://safebooru.org/images/{Directory}/{Image}";
 | 
			
		||||
 | 
			
		||||
    public string Rating { get; set; }
 | 
			
		||||
    public string Tags { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(FileUrl, Booru.Safebooru, Tags.Split(' '), Rating);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
public class SankakuImageObject : IImageData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("file_url")]
 | 
			
		||||
    public string FileUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("file_type")]
 | 
			
		||||
    public string FileType { get; set; }
 | 
			
		||||
 | 
			
		||||
    public Tag[] Tags { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("total_score")]
 | 
			
		||||
    public int Score { get; set; }
 | 
			
		||||
 | 
			
		||||
    public ImageData ToCachedImageData(Booru type)
 | 
			
		||||
        => new(FileUrl, Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString());
 | 
			
		||||
 | 
			
		||||
    public class Tag
 | 
			
		||||
    {
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Nsfw.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Common;
 | 
			
		||||
 | 
			
		||||
public class ImageCacherObject : IComparable<ImageCacherObject>
 | 
			
		||||
{
 | 
			
		||||
    public Booru SearchType { get; }
 | 
			
		||||
    public string FileUrl { get; }
 | 
			
		||||
    public HashSet<string> Tags { get; }
 | 
			
		||||
    public string Rating { get; }
 | 
			
		||||
 | 
			
		||||
    public ImageCacherObject(DapiImageObject obj, Booru type)
 | 
			
		||||
    {
 | 
			
		||||
        if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
 | 
			
		||||
            FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture)
 | 
			
		||||
                ? obj.FileUrl
 | 
			
		||||
                : "https:" + obj.FileUrl;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        SearchType = type;
 | 
			
		||||
        Rating = obj.Rating;
 | 
			
		||||
        Tags = new((obj.Tags ?? obj.TagString).Split(' '));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ImageCacherObject(
 | 
			
		||||
        string url,
 | 
			
		||||
        Booru type,
 | 
			
		||||
        string tags,
 | 
			
		||||
        string rating)
 | 
			
		||||
    {
 | 
			
		||||
        SearchType = type;
 | 
			
		||||
        FileUrl = url;
 | 
			
		||||
        Tags = new(tags.Split(' '));
 | 
			
		||||
        Rating = rating;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
        => FileUrl;
 | 
			
		||||
 | 
			
		||||
    public int CompareTo(ImageCacherObject other)
 | 
			
		||||
        => string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
 | 
			
		||||
}
 | 
			
		||||
@@ -97,10 +97,6 @@
 | 
			
		||||
        <PackageReference Include="TwitchLib.Api" Version="3.4.1" />
 | 
			
		||||
 | 
			
		||||
        <!-- Uncomment to check for disposable issues -->
 | 
			
		||||
<!--        <PackageReference Include="IDisposableAnalyzers" Version="4.0.2">-->
 | 
			
		||||
<!--            <PrivateAssets>all</PrivateAssets>-->
 | 
			
		||||
<!--            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
 | 
			
		||||
<!--        </PackageReference>-->
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
 | 
			
		||||
 | 
			
		||||
@@ -130,6 +126,18 @@
 | 
			
		||||
        <None Update="creds.yml;creds_example.yml">
 | 
			
		||||
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
        <None Update="data\lib\libsodium.dll">
 | 
			
		||||
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
        <None Update="data\lib\opus.dll">
 | 
			
		||||
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
        <None Update="data\lib\libsodium.so">
 | 
			
		||||
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
        <None Update="data\lib\libopus.so">
 | 
			
		||||
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
        </None>
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup Condition=" '$(Version)' == '' ">
 | 
			
		||||
 
 | 
			
		||||
@@ -130,21 +130,24 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
 | 
			
		||||
    public static IKernel AddLifetimeServices(this IKernel kernel)
 | 
			
		||||
    {
 | 
			
		||||
        Assembly.GetExecutingAssembly()
 | 
			
		||||
                .ExportedTypes
 | 
			
		||||
                .Where(x => x.IsPublic && x.IsClass && !x.IsAbstract)
 | 
			
		||||
        kernel.Bind(scan =>
 | 
			
		||||
        {
 | 
			
		||||
            scan.FromThisAssembly()
 | 
			
		||||
                .SelectAllClasses()
 | 
			
		||||
                .Where(c => (c.IsAssignableTo(typeof(INService))
 | 
			
		||||
                             || c.IsAssignableTo(typeof(IExecOnMessage))
 | 
			
		||||
                             || c.IsAssignableTo(typeof(IInputTransformer))
 | 
			
		||||
                             || c.IsAssignableTo(typeof(IExecPreCommand))
 | 
			
		||||
                             || c.IsAssignableTo(typeof(IExecPostCommand))
 | 
			
		||||
                             || c.IsAssignableTo(typeof(IExecNoCommand)))
 | 
			
		||||
                            && !c.HasAttribute<DontAddToIocContainerAttribute>()
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
                                                && !c.HasAttribute<NoPublicBotAttribute>()
 | 
			
		||||
                            && !c.HasAttribute<DIIgnoreAttribute>()
 | 
			
		||||
#if GLOBAL_NADEK
 | 
			
		||||
                            && !c.HasAttribute<NoPublicBotAttribute>()
 | 
			
		||||
#endif
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                )
 | 
			
		||||
                .BindToSelfWithInterfaces()
 | 
			
		||||
                .Configure(c => c.InSingletonScope());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return kernel;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user