Global usings and file scoped namespaces

This commit is contained in:
Kwoth
2021-12-19 05:14:11 +01:00
parent bc31dae965
commit ee33313519
548 changed files with 47528 additions and 49115 deletions

View File

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

View File

@@ -1,18 +1,17 @@
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class DapiImageObject : IImageData
{
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; }
[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 ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating);
}
}
public ImageData ToCachedImageData(Booru type)
=> new ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating);
}

View File

@@ -1,15 +1,14 @@
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Nsfw.Common
{
public readonly struct DapiTag
{
public string Name { get; }
namespace NadekoBot.Modules.Nsfw.Common;
[JsonConstructor]
public DapiTag(string name)
{
Name = name;
}
public readonly struct DapiTag
{
public string Name { get; }
[JsonConstructor]
public DapiTag(string name)
{
Name = name;
}
}

View File

@@ -1,19 +1,18 @@
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Nsfw.Common
{
public class DerpiContainer
{
public DerpiImageObject[] Images { get; set; }
}
namespace NadekoBot.Modules.Nsfw.Common;
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"));
}
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

@@ -4,38 +4,37 @@ using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public sealed class DanbooruImageDownloader : DapiImageDownloader
{
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 override async Task<bool> IsTagValid(string tag, CancellationToken cancel = default)
{
// 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();
if (_existentTags.ContainsKey(tag))
return true;
public override async Task<bool> IsTagValid(string tag, CancellationToken cancel = default)
if (_nonexistentTags.ContainsKey(tag))
return false;
var tags = await _http.GetFromJsonAsync<DapiTag[]>(_baseUrl +
"/tags.json" +
$"?search[name_or_alias_matches]={tag}",
options: this._serializerOptions,
cancellationToken: cancel);
if (tags is {Length: > 0})
{
if (_existentTags.ContainsKey(tag))
return true;
if (_nonexistentTags.ContainsKey(tag))
return false;
var tags = await _http.GetFromJsonAsync<DapiTag[]>(_baseUrl +
"/tags.json" +
$"?search[name_or_alias_matches]={tag}",
options: this._serializerOptions,
cancellationToken: cancel);
if (tags is {Length: > 0})
{
return _existentTags[tag] = true;
}
return _nonexistentTags[tag] = false;
return _existentTags[tag] = true;
}
return _nonexistentTags[tag] = false;
}
public DanbooruImageDownloader(HttpClient http)
: base(Booru.Danbooru, http, "http://danbooru.donmai.us")
{
}
public DanbooruImageDownloader(HttpClient http)
: base(Booru.Danbooru, http, "http://danbooru.donmai.us")
{
}
}

View File

@@ -1,56 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
{
public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
protected readonly string _baseUrl;
public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http)
{
protected readonly string _baseUrl;
_baseUrl = baseUrl;
}
public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http)
public abstract Task<bool> IsTagValid(string tag, CancellationToken cancel = default);
protected async Task<bool> AllTagsValid(string[] tags, CancellationToken cancel = default)
{
var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel)));
// if any of the tags is not valid, the query is not valid
foreach (var result in results)
{
_baseUrl = baseUrl;
if (!result)
return false;
}
public abstract Task<bool> IsTagValid(string tag, CancellationToken cancel = default);
protected async Task<bool> AllTagsValid(string[] tags, CancellationToken cancel = default)
{
var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel)));
return true;
}
// if any of the tags is not valid, the query is not valid
foreach (var result in results)
{
if (!result)
return false;
}
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();
return true;
}
if (!await AllTagsValid(tags, cancel).ConfigureAwait(false))
return new();
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();
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
if (!await AllTagsValid(tags, cancel).ConfigureAwait(false))
return new();
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
}

View File

@@ -1,35 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Nsfw.Common
{
public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
{
public DerpibooruImageDownloader(HttpClient http) : base(Booru.Derpibooru, http)
{
}
namespace NadekoBot.Modules.Nsfw.Common;
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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
{
public DerpibooruImageDownloader(HttpClient 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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var container = await res.Content.ReadFromJsonAsync<DerpiContainer>(_serializerOptions, cancel).ConfigureAwait(false);
if (container?.Images is null)
return new();
var container = await res.Content.ReadFromJsonAsync<DerpiContainer>(_serializerOptions, cancel).ConfigureAwait(false);
if (container?.Images is null)
return new();
return container.Images
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
.ToList();
}
return container.Images
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
.ToList();
}
}

View File

@@ -1,35 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class E621ImageDownloader : ImageDownloader<E621Object>
{
public class E621ImageDownloader : ImageDownloader<E621Object>
public E621ImageDownloader(HttpClient http) : base(Booru.E621, http)
{
public E621ImageDownloader(HttpClient 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: 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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
public override async Task<List<E621Object>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
{
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: 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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<E621Response>(_serializerOptions, cancel).ConfigureAwait(false);
if (data?.Posts is null)
return new();
var data = await res.Content.ReadFromJsonAsync<E621Response>(_serializerOptions, cancel).ConfigureAwait(false);
if (data?.Posts is null)
return new();
return data.Posts
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
.ToList();
}
return data.Posts
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
.ToList();
}
}

View File

@@ -1,9 +1,6 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Nsfw.Common;
namespace NadekoBot.Modules.Nsfw.Common
public class E621Response
{
public class E621Response
{
public List<E621Object> Posts { get; set; }
}
public List<E621Object> Posts { get; set; }
}

View File

@@ -1,35 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
{
public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
public GelbooruImageDownloader(HttpClient http) : base(Booru.Gelbooru, http)
{
public GelbooruImageDownloader(HttpClient 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 = $"http://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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var resString = await res.Content.ReadAsStringAsync(cancel);
if (string.IsNullOrWhiteSpace(resString))
return new();
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 = $"http://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 res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
var resString = await res.Content.ReadAsStringAsync(cancel);
if (string.IsNullOrWhiteSpace(resString))
return new();
var images = JsonSerializer.Deserialize<List<DapiImageObject>>(resString, _serializerOptions);
if (images is null)
return new();
var images = JsonSerializer.Deserialize<List<DapiImageObject>>(resString, _serializerOptions);
if (images is null)
return new();
return images.Where(x => x.FileUrl is not null).ToList();
}
return images.Where(x => x.FileUrl is not null).ToList();
}
}

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public interface IImageDownloader
{
public interface IImageDownloader
{
Task<List<ImageData>> DownloadImageDataAsync(string[] tags, int page = 0,
bool isExplicit = false, CancellationToken cancel = default);
}
Task<List<ImageData>> DownloadImageDataAsync(string[] tags, int page = 0,
bool isExplicit = false, CancellationToken cancel = default);
}

View File

@@ -1,40 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public abstract class ImageDownloader<T> : IImageDownloader
where T : IImageData
{
public abstract class ImageDownloader<T> : IImageDownloader
where T : IImageData
protected readonly HttpClient _http;
protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
{
protected readonly HttpClient _http;
protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString,
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString,
};
};
public Booru Booru { get; }
public Booru Booru { get; }
public ImageDownloader(Booru booru, HttpClient http)
{
_http = http;
this.Booru = booru;
}
public ImageDownloader(Booru booru, HttpClient http)
{
_http = http;
this.Booru = booru;
}
public abstract Task<List<T>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default);
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).ConfigureAwait(false);
return images.Select(x => x.ToCachedImageData(Booru)).ToList();
}
public async Task<List<ImageData>> DownloadImageDataAsync(string[] tags, int page, bool isExplicit = false,
CancellationToken cancel = default)
{
var images = await DownloadImagesAsync(tags, page, isExplicit, cancel).ConfigureAwait(false);
return images.Select(x => x.ToCachedImageData(Booru)).ToList();
}
}

View File

@@ -1,17 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Modules.Nsfw.Common;
namespace NadekoBot.Modules.Nsfw.Common
public static class ImageDownloaderHelper
{
public static class ImageDownloaderHelper
public static string GetTagString(IEnumerable<string> tags, bool isExplicit = false)
{
public static string GetTagString(IEnumerable<string> tags, bool isExplicit = false)
{
if (isExplicit)
tags = tags.Append("rating:explicit");
if (isExplicit)
tags = tags.Append("rating:explicit");
return string.Join('+', tags.Select(x => x.ToLowerInvariant()));
}
return string.Join('+', tags.Select(x => x.ToLowerInvariant()));
}
}

View File

@@ -1,33 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
{
public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
private readonly string _baseUrl;
public KonachanImageDownloader(HttpClient http)
: base(Booru.Konachan, http)
{
private readonly string _baseUrl;
_baseUrl = "https://konachan.com";
}
public KonachanImageDownloader(HttpClient 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}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
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}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
}

View File

@@ -1,31 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
{
public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
public Rule34ImageDownloader(HttpClient http) : base(Booru.Rule34, http)
{
public Rule34ImageDownloader(HttpClient 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://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" +
$"&tags={tagString}&pid={page}";
var images = await _http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel).ConfigureAwait(false);
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://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" +
$"&tags={tagString}&pid={page}";
var images = await _http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel).ConfigureAwait(false);
if (images is null)
return new();
if (images is null)
return new();
return images
.Where(img => !string.IsNullOrWhiteSpace(img.Image))
.ToList();
}
return images
.Where(img => !string.IsNullOrWhiteSpace(img.Image))
.ToList();
}
}

View File

@@ -1,26 +1,24 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
{
public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
public SafebooruImageDownloader(HttpClient http) : base(Booru.Safebooru, http)
{
public SafebooruImageDownloader(HttpClient 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, isExplicit: false);
var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}";
var images = await _http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancellationToken: cancel);
if (images is null)
return new();
public override async Task<List<SafebooruElement>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
{
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: false);
var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}";
var images = await _http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancellationToken: cancel);
if (images is null)
return new();
return images;
}
return images;
}
}

View File

@@ -1,34 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
{
public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
private readonly string _baseUrl;
public SankakuImageDownloader(HttpClient http)
: base(Booru.Sankaku, http)
{
private readonly string _baseUrl;
public SankakuImageDownloader(HttpClient http)
: base(Booru.Sankaku, http)
{
_baseUrl = "https://capi-v2.sankakucomplex.com";
_http.AddFakeHeaders();
}
_baseUrl = "https://capi-v2.sankakucomplex.com";
_http.AddFakeHeaders();
}
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, false);
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, false);
var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50";
var data = await _http.GetStringAsync(uri).ConfigureAwait(false);
return JsonSerializer.Deserialize<SankakuImageObject[]>(data, _serializerOptions)
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
.ToList();
}
var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50";
var data = await _http.GetStringAsync(uri).ConfigureAwait(false);
return JsonSerializer.Deserialize<SankakuImageObject[]>(data, _serializerOptions)
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
.ToList();
}
}

View File

@@ -1,34 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
{
public sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
{
private readonly string _baseUrl;
private readonly string _baseUrl;
public YandereImageDownloader(HttpClient 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);
public YandereImageDownloader(HttpClient 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}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
.ConfigureAwait(false);
if (imageObjects is null)
return new();
return imageObjects
.Where(x => x.FileUrl is not null)
.ToList();
}
}

View File

@@ -1,27 +1,26 @@
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class E621Object : IImageData
{
public class E621Object : IImageData
public class FileData
{
public class FileData
{
public string Url { get; set; }
}
public class TagData
{
public string[] General { get; set; }
}
public class ScoreData
{
public int Total { get; set; }
}
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 string Url { get; set; }
}
public class TagData
{
public string[] General { get; set; }
}
public class ScoreData
{
public int Total { get; set; }
}
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());
}

View File

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

View File

@@ -1,43 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Modules.Nsfw.Common;
namespace NadekoBot.Modules.Nsfw.Common
public class ImageData : IComparable<ImageData>
{
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)
{
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))
{
if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
this.FileUrl = "https://danbooru.donmai.us" + url;
}
else
{
this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url;
}
this.FileUrl = "https://danbooru.donmai.us" + url;
}
else
{
this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url;
}
this.SearchType = type;
this.FileUrl = url;
this.Tags = tags.ToHashSet();
this.Rating = rating;
}
public override string ToString()
{
return FileUrl;
}
public override int GetHashCode() => FileUrl.GetHashCode();
public override bool Equals(object obj)
=> obj is ImageData ico && ico.FileUrl == this.FileUrl;
public int CompareTo(ImageData other)
=> string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
this.SearchType = type;
this.FileUrl = url;
this.Tags = tags.ToHashSet();
this.Rating = rating;
}
}
public override string ToString()
{
return FileUrl;
}
public override int GetHashCode() => FileUrl.GetHashCode();
public override bool Equals(object obj)
=> obj is ImageData ico && ico.FileUrl == this.FileUrl;
public int CompareTo(ImageData other)
=> string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
}

View File

@@ -1,19 +1,18 @@
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; }
namespace NadekoBot.Modules.Nsfw.Common;
public ImageData ToCachedImageData(Booru type)
{
return new ImageData(
$"https://img.rule34.xxx//images/{Directory}/{Image}",
Booru.Rule34,
Tags.Split(' '),
Score.ToString());
}
public class Rule34Object : IImageData
{
public string Image { get; init; }
public int Directory { get; init; }
public string Tags { get; init; }
public int Score { get; init; }
public ImageData ToCachedImageData(Booru type)
{
return new ImageData(
$"https://img.rule34.xxx//images/{Directory}/{Image}",
Booru.Rule34,
Tags.Split(' '),
Score.ToString());
}
}

View File

@@ -1,15 +1,14 @@
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class SafebooruElement : IImageData
{
public class SafebooruElement : IImageData
{
public string Directory { get; set; }
public string Image { get; set; }
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 ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating);
}
}
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 ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating);
}

View File

@@ -1,27 +1,25 @@
using System.Linq;
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Nsfw.Common
{
public class SankakuImageObject : IImageData
{
public class Tag
{
public string Name { get; set; }
}
[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; }
namespace NadekoBot.Modules.Nsfw.Common;
public ImageData ToCachedImageData(Nsfw.Common.Booru type)
=> new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString());
public class SankakuImageObject : IImageData
{
public class Tag
{
public string Name { get; set; }
}
[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(Nsfw.Common.Booru type)
=> new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString());
}

View File

@@ -1,31 +1,29 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Modules.Nsfw.Common;
using NadekoBot.Modules.Searches.Common;
namespace NadekoBot.Modules.Nsfw
namespace NadekoBot.Modules.Nsfw;
public interface ISearchImagesService
{
public interface ISearchImagesService
{
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);
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);
ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; }
ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; }
ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; }
}

View File

@@ -6,422 +6,417 @@ using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Serilog;
namespace NadekoBot.Modules.Nsfw
namespace NadekoBot.Modules.Nsfw;
[NoPublicBot]
public class NSFW : NadekoModule<ISearchImagesService>
{
[NoPublicBot]
public class NSFW : NadekoModule<ISearchImagesService>
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
private readonly IHttpClientFactory _httpFactory;
private readonly NadekoRandom _rng;
public NSFW(IHttpClientFactory factory)
{
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
private readonly IHttpClientFactory _httpFactory;
private readonly NadekoRandom _rng;
_httpFactory = factory;
_rng = new NadekoRandom();
}
public NSFW(IHttpClientFactory factory)
private async Task InternalBoobs()
{
try
{
_httpFactory = factory;
_rng = new NadekoRandom();
JToken obj;
using (var http = _httpFactory.CreateClient())
{
obj = JArray.Parse(await http
.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
private async Task InternalBoobs()
catch (Exception ex)
{
try
{
JToken obj;
using (var http = _httpFactory.CreateClient())
{
obj = JArray.Parse(await http
.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
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)}")
.ConfigureAwait(false))[0];
}
await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
if (tags is null || tags.Length == 0)
await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false);
else
{
var groups = tags.Split('|');
var group = groups[_rng.Next(0, groups.Length)];
await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false);
}
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoHentaiTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.autohentai_started(
interval,
string.Join(", ", tags)));
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalBoobs().ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoBoobTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.started(interval));
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalButts(ctx.Channel).ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoButtTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.started(interval));
}
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Hentai(params string[] tags)
=> InternalDapiCommand(tags, true, _service.Hentai);
[NadekoCommand, Aliases]
[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 != null).ToArray();
if (images is null || !linksEnum.Any())
{
await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false);
return;
}
await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url)))
.ConfigureAwait(false);
}
finally
{
_hentaiBombBlacklist.TryRemove(ctx.Guild?.Id ?? ctx.User.Id);
}
}
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Yandere(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Yandere);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Konachan(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Konachan);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Sankaku(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Sankaku);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task E621(params string[] tags)
=> InternalDapiCommand(tags, false, _service.E621);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Rule34(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Rule34);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Danbooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Danbooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Gelbooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Gelbooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Derpibooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.DerpiBooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Safebooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.SafeBooru);
[NadekoCommand, Aliases]
[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)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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)
: "-").ConfigureAwait(false);
}
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));
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[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);
}
[NadekoCommand, Aliases]
[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)
.JoinWith(" ");
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), 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))}"));
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
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)}")
.ConfigureAwait(false))[0];
}
await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
if (tags is null || tags.Length == 0)
await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false);
else
{
var groups = tags.Split('|');
var group = groups[_rng.Next(0, groups.Length)];
await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false);
}
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoHentaiTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.autohentai_started(
interval,
string.Join(", ", tags)));
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalBoobs().ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoBoobTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.started(interval));
}
[NadekoCommand, Aliases]
[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).ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalButts(ctx.Channel).ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoButtTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalizedAsync(strs.started(interval));
}
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Hentai(params string[] tags)
=> InternalDapiCommand(tags, true, _service.Hentai);
[NadekoCommand, Aliases]
[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 != null).ToArray();
if (images is null || !linksEnum.Any())
{
await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false);
return;
}
await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url)))
.ConfigureAwait(false);
}
finally
{
_hentaiBombBlacklist.TryRemove(ctx.Guild?.Id ?? ctx.User.Id);
}
}
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Yandere(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Yandere);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Konachan(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Konachan);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Sankaku(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Sankaku);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task E621(params string[] tags)
=> InternalDapiCommand(tags, false, _service.E621);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Rule34(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Rule34);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Danbooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Danbooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Gelbooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.Gelbooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Derpibooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.DerpiBooru);
[NadekoCommand, Aliases]
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
public Task Safebooru(params string[] tags)
=> InternalDapiCommand(tags, false, _service.SafeBooru);
[NadekoCommand, Aliases]
[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)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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)}")
.ConfigureAwait(false))[0];
}
await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[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)
: "-").ConfigureAwait(false);
}
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));
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[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);
}
[NadekoCommand, Aliases]
[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)
.JoinWith(" ");
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), 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))}"));
}
}

View File

@@ -1,12 +1,11 @@
namespace NadekoBot.Modules.Nsfw
namespace NadekoBot.Modules.Nsfw;
public interface INsfwService
{
public interface INsfwService
{
}
}
public class NsfwService
{
public class NsfwService
{
}
}

View File

@@ -1,8 +1,4 @@
using Serilog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -11,318 +7,317 @@ using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Services;
namespace NadekoBot.Modules.Nsfw.Common
namespace NadekoBot.Modules.Nsfw.Common;
public class SearchImageCacher : INService
{
public class SearchImageCacher : INService
private readonly IHttpClientFactory _httpFactory;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly Random _rng;
private static readonly ISet<string> defaultTagBlacklist = new HashSet<string>()
{
private readonly IHttpClientFactory _httpFactory;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly Random _rng;
"loli",
"lolicon",
"shota",
"shotacon",
"cub"
};
private static readonly ISet<string> defaultTagBlacklist = new HashSet<string>()
private readonly Dictionary<Booru, object> _typeLocks = new();
private readonly Dictionary<Booru, HashSet<string>> _usedTags = new();
private readonly IMemoryCache _cache;
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>())
{
"loli",
"lolicon",
"shota",
"shotacon",
"cub"
};
private readonly Dictionary<Booru, object> _typeLocks = new();
private readonly Dictionary<Booru, HashSet<string>> _usedTags = new();
private readonly IMemoryCache _cache;
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).ConfigureAwait(false);
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 {0}...", 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)
.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, bool forceExplicit, 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, forceExplicit, 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).ConfigureAwait(false);
}
catch (HttpRequestException)
{
return default;
}
if (!success)
return default;
image = QueryLocal(tags, forceExplicit, type, blacklistedTags);
return image;
}
private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new();
public async Task<List<ImageData>> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel)
{
var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
var page = 0;
var attempt = 0;
while (attempt++ <= 10)
{
if (maxPages.TryGetValue((type, tagStr), out var maxPage))
{
if (maxPage == 0)
{
Log.Information("Tag {0} yields no result on {1}, 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).ConfigureAwait(false);
if (result is null or { Count: 0 })
{
Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString());
continue;
}
return result;
}
return new();
}
private IImageDownloader GetImageDownloader(Booru booru, HttpClient http)
=> booru switch
{
Booru.Danbooru => new DanbooruImageDownloader(http),
Booru.Yandere => new YandereImageDownloader(http),
Booru.Konachan => new KonachanImageDownloader(http),
Booru.Safebooru => new SafebooruImageDownloader(http),
Booru.E621 => new E621ImageDownloader(http),
Booru.Derpibooru => new DerpibooruImageDownloader(http),
Booru.Gelbooru => new GelbooruImageDownloader(http),
Booru.Rule34 => new Rule34ImageDownloader(http),
Booru.Sankaku => new SankakuImageDownloader(http),
_ => 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 {0} (page {1})...", type, page);
using var http = _httpFactory.CreateClient();
var downloader = GetImageDownloader(type, http);
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: {0}\nType: {1}\nPage: {2}\nMessage: {3}",
string.Join(", ", tags),
type,
page,
ex.Message);
return new();
}
_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).ConfigureAwait(false);
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 {0}...", 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)
.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, bool forceExplicit, 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, forceExplicit, 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).ConfigureAwait(false);
}
catch (HttpRequestException)
{
return default;
}
if (!success)
return default;
image = QueryLocal(tags, forceExplicit, type, blacklistedTags);
return image;
}
private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new();
public async Task<List<ImageData>> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel)
{
var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
var page = 0;
var attempt = 0;
while (attempt++ <= 10)
{
if (maxPages.TryGetValue((type, tagStr), out var maxPage))
{
if (maxPage == 0)
{
Log.Information("Tag {0} yields no result on {1}, 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).ConfigureAwait(false);
if (result is null or { Count: 0 })
{
Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString());
continue;
}
return result;
}
return new();
}
private IImageDownloader GetImageDownloader(Booru booru, HttpClient http)
=> booru switch
{
Booru.Danbooru => new DanbooruImageDownloader(http),
Booru.Yandere => new YandereImageDownloader(http),
Booru.Konachan => new KonachanImageDownloader(http),
Booru.Safebooru => new SafebooruImageDownloader(http),
Booru.E621 => new E621ImageDownloader(http),
Booru.Derpibooru => new DerpibooruImageDownloader(http),
Booru.Gelbooru => new GelbooruImageDownloader(http),
Booru.Rule34 => new Rule34ImageDownloader(http),
Booru.Sankaku => new SankakuImageDownloader(http),
_ => 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 {0} (page {1})...", type, page);
using var http = _httpFactory.CreateClient();
var downloader = GetImageDownloader(type, http);
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: {0}\nType: {1}\nPage: {2}\nMessage: {3}",
string.Join(", ", tags),
type,
page,
ex.Message);
return new();
}
}
}

View File

@@ -1,9 +1,5 @@
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;
@@ -17,377 +13,376 @@ using Newtonsoft.Json;
using Booru = NadekoBot.Modules.Nsfw.Common.Booru;
using SearchImageCacher = NadekoBot.Modules.Nsfw.Common.SearchImageCacher;
namespace NadekoBot.Modules.Nsfw
namespace NadekoBot.Modules.Nsfw;
public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags);
public record UrlReply
{
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<string> Tags { get; } = new List<string>();
}
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<ulong, HashSet<string>> BlacklistedTags { get; }
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public SearchImagesService(DbService db,
IHttpClientFactory http,
SearchImageCacher cacher,
IHttpClientFactory httpFactory)
{
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 List<string>();
_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<string>(x.Select(x => x.Tag))));
}
public class SearchImagesService : ISearchImagesService, INService
private Task<UrlReply> GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default)
{
private readonly Random _rng;
private readonly HttpClient _http;
private readonly SearchImageCacher _cache;
private readonly IHttpClientFactory _httpFactory;
private readonly DbService _db;
private ConcurrentDictionary<ulong, HashSet<string>> BlacklistedTags { get; }
return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty<string>(), forceExplicit, dapi, cancel);
}
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or /
public SearchImagesService(DbService db,
IHttpClientFactory http,
SearchImageCacher cacher,
IHttpClientFactory httpFactory)
private async Task<UrlReply> GetNsfwImageAsync(
ulong guildId,
string[] tags,
bool forceExplicit,
Booru dapi,
CancellationToken cancel)
{
if (!tags.All(x => IsValidTag(x)))
{
_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<string>(x.Select(x => x.Tag))));
return new UrlReply
{
Error = "One or more tags are invalid.",
Url = ""
};
}
private Task<UrlReply> GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default)
Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId);
try
{
return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty<string>(), forceExplicit, dapi, cancel);
}
BlacklistedTags.TryGetValue(guildId, out var blTags);
private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or /
if (dapi == Booru.E621) {
for (var i = 0; i < tags.Length; ++i)
if (tags[i] == "yuri")
tags[i] = "female/female";
}
private async Task<UrlReply> GetNsfwImageAsync(
ulong guildId,
string[] tags,
bool forceExplicit,
Booru dapi,
CancellationToken cancel)
{
if (!tags.All(x => IsValidTag(x)))
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)
.ConfigureAwait(false);
if (result is null)
{
return new UrlReply
{
Error = "One or more tags are invalid.",
Error = "Image not found.",
Url = ""
};
}
Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId);
try
var reply = new UrlReply
{
BlacklistedTags.TryGetValue(guildId, out var blTags);
Error = "",
Url = result.FileUrl,
Rating = result.Rating,
Provider = result.SearchType.ToString()
};
if (dapi == Booru.E621) {
for (var i = 0; i < tags.Length; ++i)
if (tags[i] == "yuri")
tags[i] = "female/female";
}
reply.Tags.AddRange(result.Tags);
if (dapi == Booru.Derpibooru)
{
for (var i = 0; i < tags.Length; ++i)
if (tags[i] == "yuri")
tags[i] = "lesbian";
}
return reply;
var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet<string>(), 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 = ""
};
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message);
return new UrlReply
{
Error = ex.Message,
Url = ""
};
}
}
public Task<UrlReply> Gelbooru(ulong? guildId, bool forceExplicit, string[] tags)
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru);
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> 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> 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> 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> 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> 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> 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> 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 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)
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
{
var providers = new[] {
Booru.Danbooru,
Booru.Konachan,
Booru.Gelbooru,
Booru.Yandere
};
// wait for any of the tasks to complete
var task = await Task.WhenAny(tasks);
using var cancelSource = new CancellationTokenSource();
// create a task for each type
var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList();
do
// get its result
var result = task.GetAwaiter().GetResult();
if(result.Error == "")
{
// 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);
// if we have a non-error result, cancel other searches and return the result
cancelSource.Cancel();
return result;
}
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()
// 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<UrlReply> 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 = "No hentai image found."
Error = "",
Url = $"http://media.oboobs.ru/{obj["preview"]}",
};
}
public async Task<UrlReply> Boobs()
catch (Exception ex)
{
try
Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message);
return new UrlReply
{
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 = "",
};
}
Error = ex.Message,
Url = "",
};
}
}
private readonly object taglock = new object();
public ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag)
private readonly object taglock = new object();
public ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag)
{
lock (taglock)
{
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)
{
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 NsfwBlacklistedTag()
{
Tag = tag,
GuildId = guildId
});
uow.SaveChanges();
}
return new(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<string[]> GetBlacklistedTags(ulong guildId)
public ValueTask<string[]> GetBlacklistedTags(ulong guildId)
{
lock (taglock)
{
lock (taglock)
if (BlacklistedTags.TryGetValue(guildId, out var tags))
{
if (BlacklistedTags.TryGetValue(guildId, out var tags))
{
return new(tags.ToArray());
}
return new(Array.Empty<string>());
return new(tags.ToArray());
}
}
public async Task<UrlReply> Butts()
return new(Array.Empty<string>());
}
}
public async Task<UrlReply> Butts()
{
try
{
try
JToken obj;
obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0];
return new UrlReply
{
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"
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 = "",
};
}
}
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
#region Nhentai
private string GetNhentaiExtensionInternal(string s)
=> s switch
{
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<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
{
using var http = _httpFactory.CreateClient();
try
{
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with id {NhentaiId} not found", id);
return null;
}
}
private async Task<NhentaiApiModel.Gallery[]> 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<NhentaiApiModel.SearchResult>(res).Result;
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
return null;
}
}
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
{
var model = await GetNhentaiByIdInternalAsync(id);
return ModelToGallery(model);
}
private static readonly string[] _bannedTags =
{
"loli",
"lolicon",
"shota",
"shotacon",
"cub"
"j" => "jpg",
"p" => "png",
"g" => "gif",
_ => "jpg"
};
public async Task<Gallery> GetNhentaiBySearchAsync(string search)
{
var models = await SearchNhentaiInternalAsync(search);
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
{
var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb."
+ GetNhentaiExtensionInternal(model.Images.Thumbnail.T);
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
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<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
{
using var http = _httpFactory.CreateClient();
try
{
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with id {NhentaiId} not found", id);
return null;
}
}
private async Task<NhentaiApiModel.Gallery[]> 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<NhentaiApiModel.SearchResult>(res).Result;
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
return null;
}
}
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
{
var model = await GetNhentaiByIdInternalAsync(id);
return ModelToGallery(model);
}
private static readonly string[] _bannedTags =
{
"loli",
"lolicon",
"shota",
"shotacon",
"cub"
};
public async Task<Gallery> 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
}