1
Fork 0
mirror of https://gitlab.com/Kwoth/nadekobot.git synced 2024-10-02 12:09:07 +00:00

Removing bloat, fixing file names

This commit is contained in:
Kwoth 2023-03-11 09:10:37 +01:00
parent 8c8e9f7770
commit 1ad0bc33af
72 changed files with 210 additions and 2157 deletions

View file

@ -2,6 +2,11 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.0.0] - ??.??.????
- Big internal changes
## [4.3.13] - 20.02.2023
### Fixed

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source
COPY src/Nadeko.Medusa/*.csproj src/Nadeko.Medusa/

View file

@ -1,4 +1,4 @@
Copyright 2021 Kwoth
Copyright 2023 Kwoth
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -1,63 +0,0 @@
function Get-Changelog($lastTag)
{
if(!$lastTag)
{
$lastTag = git describe --tags --abbrev=0
}
$tag = "$lastTag..HEAD"
$clArr = (git log $tag --oneline)
[array]::Reverse($clArr)
$changelog = $clArr | where { "$_" -notlike "*(POEditor.com)*" -and "$_" -notlike "*Merge branch*" -and "$_" -notlike "*Merge pull request*" -and "$_" -notlike "^-*" -and "$_" -notlike "*Merge remote tracking*" }
$changelog = [string]::join([Environment]::NewLine, $changelog)
$cl2 = $clArr | where { "$_" -like "*Merge pull request*" }
$changelog = "## Changes$nl$changelog"
if ($null -ne $cl2) {
$cl2 = [string]::join([Environment]::NewLine, $cl2)
$changelog = $changelog + "$nl ## Pull Requests Merged$nl$cl2"
}
return $changelog
}
function Build-Installer($versionNumber)
{
$env:NADEKOBOT_INSTALL_VERSION = $versionNumber
dotnet clean
# rm -r -fo "src\NadekoBot\bin"
dotnet publish -c Release --runtime win7-x64 /p:Version=$versionNumber src/NadekoBot
# .\rcedit-x64.exe "src\NadekoBot\bin\Release\netcoreapp2.1\win7-x64\nadekobot.exe" --set-icon "src\NadekoBot\bin\Release\netcoreapp2.1\win7-x64\nadeko_icon.ico"
& "iscc.exe" "/O+" ".\exe_builder.iss"
Write-ReleaseFile($versionNumber)
# $path = [Environment]::GetFolderPath('MyDocuments') + "\_projekti\new_installer\$versionNumber\";
# $binPath = $path + "nadeko-setup-$versionNumber.exe";
# Copy-Item -Path $path -Destination $dest -Force -ErrorAction Stop
# return $path
}
function Write-ReleaseFile($versionNumber) {
$changelog = ""
# pull the changes if they exist
# git pull
# attempt to build teh installer
# $path = Build-Installer $versionNumber
# get changelog before tagging
$changelog = Get-Changelog
# tag the release
# & (git tag, $tag)
# print out the changelog to the console
# Write-Host $changelog
$jsonReleaseFile = "[{""VersionName"": ""$versionNumber"", ""DownloadLink"": ""https://cdn.nadeko.bot/dl/bot/nadeko-setup-$versionNumber.exe"", ""Changelog"": """"}]"
$releaseJsonOutPath = [Environment]::GetFolderPath('MyDocuments') + "\_projekti\nadeko-installers\$versionNumber\"
New-Item -Path $releaseJsonOutPath -Value $jsonReleaseFile -Name "releases.json" -Force
}

View file

@ -1,19 +0,0 @@
namespace Nadeko.Common;
public readonly struct ShmartBankAmount
{
public long Amount { get; }
public ShmartBankAmount(long amount)
{
Amount = amount;
}
public static implicit operator ShmartBankAmount(long num)
=> new(num);
public static implicit operator long(ShmartBankAmount num)
=> num.Amount;
public static implicit operator ShmartBankAmount(int num)
=> new(num);
}

View file

@ -1,40 +0,0 @@
using System.Numerics;
namespace Nadeko.Common;
public readonly struct ShmartNumber : IEquatable<ShmartNumber>
{
public long Value { get; }
public ShmartNumber(long val)
{
Value = val;
}
public static implicit operator ShmartNumber(long num)
=> new(num);
public static implicit operator long(ShmartNumber num)
=> num.Value;
public static implicit operator ShmartNumber(int num)
=> new(num);
public override string ToString()
=> Value.ToString();
public override bool Equals(object? obj)
=> obj is ShmartNumber sn && Equals(sn);
public bool Equals(ShmartNumber other)
=> other.Value == Value;
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(ShmartNumber left, ShmartNumber right)
=> left.Equals(right);
public static bool operator !=(ShmartNumber left, ShmartNumber right)
=> !(left == right);
}

View file

@ -15,6 +15,6 @@
</ItemGroup>
<PropertyGroup Condition=" '$(Version)' == '' ">
<Version>5.0.0</Version>
<Version>6.0.0</Version>
</PropertyGroup>
</Project>

View file

@ -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))
var filteredTypes = assembly.GetTypes()
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType?.GetGenericArguments().Length > 0
&& !x.IsAbstract);
var toReturn = new List<object>();
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)

View file

@ -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
{
}

View file

@ -19,20 +19,3 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
#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
}
}

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

View file

@ -1,6 +1,6 @@
#nullable enable
[DontAddToIocContainer]
[DIIgnore]
public sealed class BehaviorAdapter : ICustomBehavior
{
private readonly WeakReference<Snek> _snekWr;

View file

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

View file

@ -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);
}
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 TypeReaderResult.FromError<ShmartNumber>(CommandError.Unsuccessful, err.Value);
return Discord.Commands.TypeReaderResult.FromError(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);
}
}

View file

@ -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;

View file

@ -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
{

View file

@ -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;

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

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

View file

@ -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}
$"""
`Sneks:` {medusa.Sneks.Count}
`Commands:` {medusa.Sneks.Sum(x => x.Commands.Count)}
--
{medusa.Description}");
{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);
}
}

View file

@ -0,0 +1,6 @@
public class ModuleItem
{
public string Name { get; init; }
public string Description { get; init; }
public string Command { get; init; }
}

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

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

View file

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

View file

@ -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);
// }

View file

@ -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>();
// }
// }
// }

View file

@ -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

View file

@ -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();
}
}
}

View file

@ -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
*/
}

View file

@ -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();
}

View file

@ -1,15 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Nsfw.Common;
public enum Booru
{
Safebooru,
E621,
Derpibooru,
Rule34,
Gelbooru,
Konachan,
Yandere,
Danbooru,
Sankaku
}

View file

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

View file

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

View file

@ -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"));
}

View file

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

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -1,7 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Nsfw.Common;
public class E621Response
{
public List<E621Object> Posts { get; set; }
}

View file

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

View file

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

View file

@ -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();
}
}

View file

@ -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()));
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

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

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

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

View file

@ -1,7 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Nsfw.Common;
public interface IImageData
{
ImageData ToCachedImageData(Booru type);
}

View file

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

View file

@ -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());
}

View file

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

View file

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

View file

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

View file

@ -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)' == '' ">

View file

@ -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<DIIgnoreAttribute>()
#if GLOBAL_NADEK
&& !c.HasAttribute<NoPublicBotAttribute>()
#endif
);
)
.BindToSelfWithInterfaces()
.Configure(c => c.InSingletonScope());
});
return kernel;
}