using Newtonsoft.Json.Linq; using Serilog; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Threading; using LinqToDB; using NadekoBot.Common; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Common; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; using Booru = NadekoBot.Modules.Nsfw.Common.Booru; using SearchImageCacher = NadekoBot.Modules.Nsfw.Common.SearchImageCacher; namespace NadekoBot.Modules.Nsfw { public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags); 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 Tags { get; } = new List(); } public class SearchImagesService : ISearchImagesService, INService { private readonly Random _rng; private readonly HttpClient _http; private readonly SearchImageCacher _cache; private readonly IHttpClientFactory _httpFactory; private readonly DbService _db; private ConcurrentDictionary> BlacklistedTags { get; } public ConcurrentDictionary AutoHentaiTimers { get; } = new ConcurrentDictionary(); public ConcurrentDictionary AutoBoobTimers { get; } = new ConcurrentDictionary(); public ConcurrentDictionary AutoButtTimers { get; } = new ConcurrentDictionary(); public SearchImagesService(DbService db, IHttpClientFactory http, SearchImageCacher cacher, IHttpClientFactory httpFactory) { _db = db; _rng = new NadekoRandom(); _http = http.CreateClient(); _http.AddFakeHeaders(); _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(x.Select(x => x.Tag)))); } private Task GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default) { return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty(), forceExplicit, dapi, cancel); } private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or / private async Task GetNsfwImageAsync( ulong guildId, string[] tags, bool forceExplicit, Booru dapi, CancellationToken cancel) { if (!tags.All(x => IsValidTag(x))) { return new UrlReply { 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(), cancel) .ConfigureAwait(false); if (result is null) { return new UrlReply { 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 UrlReply { Error = ex.Message, Url = "" }; } } public Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru); public Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru); public Task Konachan(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan); public Task Yandere(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere); public Task Rule34(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34); public Task E621(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621); public Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru); public Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru); public Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags) => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku); public async Task 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 UrlReply() { Error = "No hentai image found." }; } public async Task Boobs() { try { JToken obj; obj = JArray.Parse(await _http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}").ConfigureAwait(false))[0]; return new UrlReply { Error = "", Url = $"http://media.oboobs.ru/{obj["preview"]}", }; } catch (Exception ex) { Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message); return new UrlReply { Error = ex.Message, Url = "", }; } } private readonly object taglock = new object(); public ValueTask ToggleBlacklistTag(ulong guildId, string tag) { lock (taglock) { tag = tag.Trim().ToLowerInvariant(); var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet()); 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 NsfwBlacklistedTag() { Tag = tag, GuildId = guildId }); uow.SaveChanges(); } return new(isAdded); } } public ValueTask GetBlacklistedTags(ulong guildId) { lock (taglock) { if (BlacklistedTags.TryGetValue(guildId, out var tags)) { return new(tags.ToArray()); } return new(Array.Empty()); } } public async Task Butts() { try { JToken obj; obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0]; return new UrlReply { Error = "", Url = $"http://media.obutts.ru/{obj["preview"]}", }; } catch (Exception ex) { Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message); return new UrlReply { Error = ex.Message, Url = "", }; } } #region Nhentai private string GetNhentaiExtensionInternal(string s) => s switch { "j" => "jpg", "p" => "png", "g" => "gif", _ => "jpg" }; private Gallery ModelToGallery(NhentaiApiModel.Gallery model) { var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb." + GetNhentaiExtensionInternal(model.Images.Thumbnail.T); var url = $"https://nhentai.net/g/{model.Id}"; return new Gallery( model.Id.ToString(), url, model.Title.English, model.Title.Pretty, thumbnail, model.NumPages, model.NumFavorites, model.UploadDate.ToUnixTimestamp().UtcDateTime, model.Tags.Map(x => new Tag() { Name = x.Name, Url = "https://nhentai.com/" + x.Url })); } private async Task GetNhentaiByIdInternalAsync(uint id) { using var http = _httpFactory.CreateClient(); try { var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id); return JsonConvert.DeserializeObject(res); } catch (HttpRequestException) { Log.Warning("Nhentai with id {NhentaiId} not found", id); return null; } } private async Task SearchNhentaiInternalAsync(string search) { using var http = _httpFactory.CreateClient(); try { var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search); return JsonConvert.DeserializeObject(res).Result; } catch (HttpRequestException) { Log.Warning("Nhentai with search {NhentaiSearch} not found", search); return null; } } public async Task GetNhentaiByIdAsync(uint id) { var model = await GetNhentaiByIdInternalAsync(id); return ModelToGallery(model); } private static readonly string[] _bannedTags = { "loli", "lolicon", "shota", "shotacon", "cub" }; public async Task GetNhentaiBySearchAsync(string search) { var models = await SearchNhentaiInternalAsync(search); models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray(); if (models.Length == 0) return null; return ModelToGallery(models[_rng.Next(0, models.Length)]); } #endregion } }