mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04: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