mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 17:58:26 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using NadekoBot.Modules.Searches.GoogleScrape;
|
||||
using NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INService
|
||||
{
|
||||
private readonly SearchesConfigService _scs;
|
||||
private readonly SearxSearchService _sss;
|
||||
private readonly GoogleSearchService _gss;
|
||||
|
||||
private readonly YtdlpYoutubeSearchService _ytdlp;
|
||||
private readonly YtdlYoutubeSearchService _ytdl;
|
||||
private readonly YoutubeDataApiSearchService _ytdata;
|
||||
private readonly InvidiousYtSearchService _iYtSs;
|
||||
private readonly GoogleScrapeService _gscs;
|
||||
|
||||
public DefaultSearchServiceFactory(
|
||||
SearchesConfigService scs,
|
||||
GoogleSearchService gss,
|
||||
GoogleScrapeService gscs,
|
||||
SearxSearchService sss,
|
||||
YtdlpYoutubeSearchService ytdlp,
|
||||
YtdlYoutubeSearchService ytdl,
|
||||
YoutubeDataApiSearchService ytdata,
|
||||
InvidiousYtSearchService iYtSs)
|
||||
{
|
||||
_scs = scs;
|
||||
_sss = sss;
|
||||
_gss = gss;
|
||||
_gscs = gscs;
|
||||
_iYtSs = iYtSs;
|
||||
|
||||
_ytdlp = ytdlp;
|
||||
_ytdl = ytdl;
|
||||
_ytdata = ytdata;
|
||||
}
|
||||
|
||||
public ISearchService GetSearchService(string? hint = null)
|
||||
=> _scs.Data.WebSearchEngine switch
|
||||
{
|
||||
WebSearchEngine.Google => _gss,
|
||||
WebSearchEngine.Google_Scrape => _gscs,
|
||||
WebSearchEngine.Searx => _sss,
|
||||
_ => _gss
|
||||
};
|
||||
|
||||
public ISearchService GetImageSearchService(string? hint = null)
|
||||
=> _scs.Data.ImgSearchEngine switch
|
||||
{
|
||||
ImgSearchEngine.Google => _gss,
|
||||
ImgSearchEngine.Searx => _sss,
|
||||
_ => _gss
|
||||
};
|
||||
|
||||
public IYoutubeSearchService GetYoutubeSearchService(string? hint = null)
|
||||
=> _scs.Data.YtProvider switch
|
||||
{
|
||||
YoutubeSearcher.YtDataApiv3 => _ytdata,
|
||||
YoutubeSearcher.Ytdlp => _ytdlp,
|
||||
YoutubeSearcher.Ytdl => _ytdl,
|
||||
YoutubeSearcher.Invidious => _iYtSs,
|
||||
_ => _ytdl
|
||||
};
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
// using AngleSharp.Html.Dom;
|
||||
// using MorseCode.ITask;
|
||||
// using NadekoBot.Modules.Searches.Common;
|
||||
// using System.Net;
|
||||
//
|
||||
// namespace NadekoBot.Modules.Searches.DuckDuckGo;
|
||||
//
|
||||
// public sealed class DuckDuckGoSeachService : SearchServiceBase
|
||||
// {
|
||||
// private static readonly HtmlParser _googleParser = new(new()
|
||||
// {
|
||||
// IsScripting = false,
|
||||
// IsEmbedded = false,
|
||||
// IsSupportingProcessingInstructions = false,
|
||||
// IsKeepingSourceReferences = false,
|
||||
// IsNotSupportingFrames = true
|
||||
// });
|
||||
//
|
||||
// public override async ITask<SearchResultData> SearchAsync(string query)
|
||||
// {
|
||||
// query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
|
||||
//
|
||||
// var fullQueryLink = "https://html.duckduckgo.com/html";
|
||||
//
|
||||
// using var http = _httpFactory.CreateClient();
|
||||
// http.DefaultRequestHeaders.Clear();
|
||||
// http.DefaultRequestHeaders.Add("User-Agent",
|
||||
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
|
||||
//
|
||||
// using var formData = new MultipartFormDataContent();
|
||||
// formData.Add(new StringContent(query), "q");
|
||||
// using var response = await http.PostAsync(fullQueryLink, formData);
|
||||
// var content = await response.Content.ReadAsStringAsync();
|
||||
//
|
||||
// using var document = await _googleParser.ParseDocumentAsync(content);
|
||||
// var searchResults = document.QuerySelector(".results");
|
||||
// var elems = searchResults.QuerySelectorAll(".result");
|
||||
//
|
||||
// if (!elems.Any())
|
||||
// return default;
|
||||
//
|
||||
// var results = elems.Select(elem =>
|
||||
// {
|
||||
// if (elem.QuerySelector(".result__a") is not IHtmlAnchorElement anchor)
|
||||
// return null;
|
||||
//
|
||||
// var href = anchor.Href;
|
||||
// var name = anchor.TextContent;
|
||||
//
|
||||
// if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(name))
|
||||
// return null;
|
||||
//
|
||||
// var txt = elem.QuerySelector(".result__snippet")?.TextContent;
|
||||
//
|
||||
// if (string.IsNullOrWhiteSpace(txt))
|
||||
// return null;
|
||||
//
|
||||
// return new GoogleSearchResult(name, href, txt);
|
||||
// })
|
||||
// .Where(x => x is not null)
|
||||
// .ToList();
|
||||
//
|
||||
// return new(results.AsReadOnly(), fullQueryLink, "0");
|
||||
// }
|
||||
// }
|
@@ -0,0 +1,22 @@
|
||||
using NadekoBot.Modules.Searches;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GoogleCustomSearchResult : ISearchResult
|
||||
{
|
||||
ISearchResultInformation ISearchResult.Info
|
||||
=> Info;
|
||||
|
||||
public string? Answer
|
||||
=> null;
|
||||
|
||||
IReadOnlyCollection<ISearchResultEntry> ISearchResult.Entries
|
||||
=> Entries ?? Array.Empty<OfficialGoogleSearchResultEntry>();
|
||||
|
||||
[JsonPropertyName("searchInformation")]
|
||||
public GoogleSearchResultInformation Info { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("items")]
|
||||
public IReadOnlyCollection<OfficialGoogleSearchResultEntry>? Entries { get; init; }
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GoogleImageData
|
||||
{
|
||||
[JsonPropertyName("contextLink")]
|
||||
public string ContextLink { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("thumbnailLink")]
|
||||
public string ThumbnailLink { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using NadekoBot.Modules.Searches;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GoogleImageResult : IImageSearchResult
|
||||
{
|
||||
ISearchResultInformation IImageSearchResult.Info
|
||||
=> Info;
|
||||
|
||||
IReadOnlyCollection<IImageSearchResultEntry> IImageSearchResult.Entries
|
||||
=> Entries ?? Array.Empty<GoogleImageResultEntry>();
|
||||
|
||||
[JsonPropertyName("searchInformation")]
|
||||
public GoogleSearchResultInformation Info { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("items")]
|
||||
public IReadOnlyCollection<GoogleImageResultEntry>? Entries { get; init; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using NadekoBot.Modules.Searches;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GoogleImageResultEntry : IImageSearchResultEntry
|
||||
{
|
||||
[JsonPropertyName("link")]
|
||||
public string Link { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("image")]
|
||||
public GoogleImageData Image { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using NadekoBot.Modules.Searches;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GoogleSearchResultInformation : ISearchResultInformation
|
||||
{
|
||||
[JsonPropertyName("formattedTotalResults")]
|
||||
public string TotalResults { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("formattedSearchTime")]
|
||||
public string SearchTime { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class GoogleSearchService : SearchServiceBase, INService
|
||||
{
|
||||
private readonly IBotCredsProvider _creds;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public GoogleSearchService(IBotCredsProvider creds, IHttpClientFactory httpFactory)
|
||||
{
|
||||
_creds = creds;
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
public override async ITask<GoogleImageResult?> SearchImagesAsync(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var creds = _creds.GetCreds();
|
||||
var key = creds.Google.ImageSearchId;
|
||||
var cx = string.IsNullOrWhiteSpace(key)
|
||||
? "c3f56de3be2034c07"
|
||||
: key;
|
||||
|
||||
using var http = _httpFactory.CreateClient("google:search");
|
||||
http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
|
||||
await using var stream = await http.GetStreamAsync(
|
||||
$"https://customsearch.googleapis.com/customsearch/v1"
|
||||
+ $"?cx={cx}"
|
||||
+ $"&q={Uri.EscapeDataString(query)}"
|
||||
+ $"&fields=items(image(contextLink%2CthumbnailLink)%2Clink)%2CsearchInformation"
|
||||
+ $"&key={creds.GoogleApiKey}"
|
||||
+ $"&searchType=image"
|
||||
+ $"&safe=active");
|
||||
|
||||
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleImageResult>(stream);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async ITask<GoogleCustomSearchResult?> SearchAsync(string? query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var creds = _creds.GetCreds();
|
||||
var key = creds.Google.SearchId;
|
||||
var cx = string.IsNullOrWhiteSpace(key)
|
||||
? "c7f1dac95987d4571"
|
||||
: key;
|
||||
|
||||
using var http = _httpFactory.CreateClient("google:search");
|
||||
http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
|
||||
await using var stream = await http.GetStreamAsync(
|
||||
$"https://customsearch.googleapis.com/customsearch/v1"
|
||||
+ $"?cx={cx}"
|
||||
+ $"&q={Uri.EscapeDataString(query)}"
|
||||
+ $"&fields=items(title%2Clink%2CdisplayLink%2Csnippet)%2CsearchInformation"
|
||||
+ $"&key={creds.GoogleApiKey}"
|
||||
+ $"&safe=active");
|
||||
|
||||
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleCustomSearchResult>(stream);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using NadekoBot.Modules.Searches;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class OfficialGoogleSearchResultEntry : ISearchResultEntry
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("link")]
|
||||
public string Url { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("displayLink")]
|
||||
public string DisplayUrl { get; init; } = null!;
|
||||
|
||||
[JsonPropertyName("snippet")]
|
||||
public string Description { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.GoogleScrape;
|
||||
|
||||
public sealed class GoogleScrapeService : SearchServiceBase, INService
|
||||
{
|
||||
private static readonly HtmlParser _googleParser = new(new()
|
||||
{
|
||||
IsScripting = false,
|
||||
IsEmbedded = false,
|
||||
IsSupportingProcessingInstructions = false,
|
||||
IsKeepingSourceReferences = false,
|
||||
IsNotSupportingFrames = true
|
||||
});
|
||||
|
||||
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public GoogleScrapeService(IHttpClientFactory httpClientFactory)
|
||||
=> _httpFactory = httpClientFactory;
|
||||
|
||||
public override async ITask<ISearchResult?> SearchAsync(string? query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
query = Uri.EscapeDataString(query)?.Replace(' ', '+');
|
||||
|
||||
var fullQueryLink = $"https://www.google.ca/search?q={query}&safe=on&lr=lang_eng&hl=en&ie=utf-8&oe=utf-8";
|
||||
|
||||
using var msg = new HttpRequestMessage(HttpMethod.Get, fullQueryLink);
|
||||
msg.Headers.Add("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
|
||||
msg.Headers.Add("Cookie", "CONSENT=YES+shp.gws-20210601-0-RC2.en+FX+423;");
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
|
||||
using var response = await http.SendAsync(msg);
|
||||
await using var content = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
using var document = await _googleParser.ParseDocumentAsync(content);
|
||||
var elems = document.QuerySelectorAll("div.g, div.mnr-c > div > div");
|
||||
|
||||
var resultsElem = document.QuerySelector("#result-stats");
|
||||
var resultsArr = resultsElem?.TextContent.Split("results");
|
||||
var totalResults = resultsArr?.Length is null or 0
|
||||
? null
|
||||
: resultsArr[0];
|
||||
|
||||
var time = resultsArr is null or {Length: < 2}
|
||||
? null
|
||||
: resultsArr[1]
|
||||
.Replace("(", string.Empty)
|
||||
.Replace("seconds)", string.Empty);
|
||||
|
||||
//var time = resultsElem.Children.FirstOrDefault()?.TextContent
|
||||
//^ this doesn't work for some reason, <nobr> is completely missing in parsed collection
|
||||
if (!elems.Any())
|
||||
return default;
|
||||
|
||||
var results = elems.Select(elem =>
|
||||
{
|
||||
var aTag = elem.QuerySelector("a");
|
||||
|
||||
if (aTag is null)
|
||||
return null;
|
||||
|
||||
var url = ((IHtmlAnchorElement)aTag).Href;
|
||||
var title = aTag.QuerySelector("h3")?.TextContent;
|
||||
|
||||
var txt = aTag.ParentElement
|
||||
?.NextElementSibling
|
||||
?.QuerySelector("span")
|
||||
?.TextContent
|
||||
.StripHtml()
|
||||
?? elem
|
||||
?.QuerySelectorAll("span")
|
||||
.Skip(3)
|
||||
.FirstOrDefault()
|
||||
?.TextContent
|
||||
.StripHtml();
|
||||
// .Select(x => x.TextContent.StripHtml())
|
||||
// .Join("\n");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url)
|
||||
|| string.IsNullOrWhiteSpace(title)
|
||||
|| string.IsNullOrWhiteSpace(txt))
|
||||
return null;
|
||||
|
||||
return new PlainSearchResultEntry
|
||||
{
|
||||
Title = title,
|
||||
Url = url,
|
||||
DisplayUrl = url,
|
||||
Description = txt
|
||||
};
|
||||
})
|
||||
.Where(x => x is not null)
|
||||
.ToList();
|
||||
|
||||
// return new GoogleSearchResult(results.AsReadOnly(), fullQueryLink, totalResults);
|
||||
|
||||
return new PlainGoogleScrapeSearchResult()
|
||||
{
|
||||
Answer = null,
|
||||
Entries = results!,
|
||||
Info = new PlainSearchResultInfo()
|
||||
{
|
||||
SearchTime = time ?? "?",
|
||||
TotalResults = totalResults ?? "?"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// someone can mr this
|
||||
public override ITask<IImageSearchResult?> SearchImagesAsync(string query)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Searches.GoogleScrape;
|
||||
|
||||
public class PlainGoogleScrapeSearchResult : ISearchResult
|
||||
{
|
||||
public string? Answer { get; init; } = null!;
|
||||
public IReadOnlyCollection<ISearchResultEntry> Entries { get; init; } = null!;
|
||||
public ISearchResultInformation Info { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Searches.GoogleScrape;
|
||||
|
||||
public sealed class PlainSearchResultEntry : ISearchResultEntry
|
||||
{
|
||||
public string Title { get; init; } = null!;
|
||||
public string Url { get; init; } = null!;
|
||||
public string DisplayUrl { get; init; } = null!;
|
||||
public string? Description { get; init; } = null!;
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches.GoogleScrape;
|
||||
|
||||
public sealed class PlainSearchResultInfo : ISearchResultInformation
|
||||
{
|
||||
public string TotalResults { get; init; } = null!;
|
||||
public string SearchTime { get; init; } = null!;
|
||||
}
|
13
src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs
Normal file
13
src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface IImageSearchResult
|
||||
{
|
||||
ISearchResultInformation Info { get; }
|
||||
|
||||
IReadOnlyCollection<IImageSearchResultEntry> Entries { get; }
|
||||
}
|
||||
|
||||
public interface IImageSearchResultEntry
|
||||
{
|
||||
string Link { get; }
|
||||
}
|
8
src/NadekoBot/Modules/Searches/Search/ISearchResult.cs
Normal file
8
src/NadekoBot/Modules/Searches/Search/ISearchResult.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface ISearchResult
|
||||
{
|
||||
string? Answer { get; }
|
||||
IReadOnlyCollection<ISearchResultEntry> Entries { get; }
|
||||
ISearchResultInformation Info { get; }
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface ISearchResultEntry
|
||||
{
|
||||
string Title { get; }
|
||||
string Url { get; }
|
||||
string DisplayUrl { get; }
|
||||
string? Description { get; }
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface ISearchResultInformation
|
||||
{
|
||||
string TotalResults { get; }
|
||||
string SearchTime { get; }
|
||||
}
|
9
src/NadekoBot/Modules/Searches/Search/ISearchService.cs
Normal file
9
src/NadekoBot/Modules/Searches/Search/ISearchService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface ISearchService
|
||||
{
|
||||
ITask<ISearchResult?> SearchAsync(string? query);
|
||||
ITask<IImageSearchResult?> SearchImagesAsync(string query);
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
using NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public interface ISearchServiceFactory
|
||||
{
|
||||
public ISearchService GetSearchService(string? hint = null);
|
||||
public ISearchService GetImageSearchService(string? hint = null);
|
||||
public IYoutubeSearchService GetYoutubeSearchService(string? hint = null);
|
||||
}
|
202
src/NadekoBot/Modules/Searches/Search/SearchCommands.cs
Normal file
202
src/NadekoBot/Modules/Searches/Search/SearchCommands.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public partial class Searches
|
||||
{
|
||||
public partial class SearchCommands : NadekoModule
|
||||
{
|
||||
private readonly ISearchServiceFactory _searchFactory;
|
||||
private readonly IBotCache _cache;
|
||||
|
||||
public SearchCommands(
|
||||
ISearchServiceFactory searchFactory,
|
||||
IBotCache cache)
|
||||
{
|
||||
_searchFactory = searchFactory;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Google([Leftover] string? query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
await ErrorLocalizedAsync(strs.specify_search_params);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var search = _searchFactory.GetSearchService();
|
||||
var data = await search.SearchAsync(query);
|
||||
|
||||
if (data is null or { Entries: null or { Count: 0 } })
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.no_results);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3 with an answer
|
||||
// 4 without an answer
|
||||
// 5 is ideal but it lookes horrible on mobile
|
||||
|
||||
var takeCount = string.IsNullOrWhiteSpace(data.Answer)
|
||||
? 4
|
||||
: 3;
|
||||
|
||||
var descStr = data.Entries
|
||||
.Take(takeCount)
|
||||
.Select(static res => $@"**[{Format.Sanitize(res.Title)}]({res.Url})**
|
||||
*{Format.EscapeUrl(res.DisplayUrl)}*
|
||||
{Format.Sanitize(res.Description ?? "-")}")
|
||||
.Join("\n\n");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.Answer))
|
||||
descStr = Format.Code(data.Answer) + "\n\n" + descStr;
|
||||
|
||||
descStr = descStr.TrimTo(4096);
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(query.TrimTo(64)!)
|
||||
.WithDescription(descStr)
|
||||
.WithFooter(
|
||||
GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
|
||||
"https://i.imgur.com/G46fm8J.png");
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Image([Leftover] string? query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
await ErrorLocalizedAsync(strs.specify_search_params);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var search = _searchFactory.GetImageSearchService();
|
||||
var data = await search.SearchImagesAsync(query);
|
||||
|
||||
if (data is null or { Entries: null or { Count: 0 } })
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.no_search_results);
|
||||
return;
|
||||
}
|
||||
|
||||
var embeds = new List<IEmbedBuilder>(4);
|
||||
|
||||
|
||||
IEmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
|
||||
{
|
||||
return _eb.Create(ctx)
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(query)
|
||||
.WithUrl("https://google.com")
|
||||
.WithImageUrl(entry.Link);
|
||||
}
|
||||
|
||||
embeds.Add(CreateEmbed(data.Entries.First())
|
||||
.WithFooter(
|
||||
GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
|
||||
"https://i.imgur.com/G46fm8J.png"));
|
||||
|
||||
var random = data.Entries.Skip(1)
|
||||
.Shuffle()
|
||||
.Take(3)
|
||||
.ToArray();
|
||||
|
||||
foreach (var entry in random)
|
||||
{
|
||||
embeds.Add(CreateEmbed(entry));
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(null, embeds: embeds);
|
||||
}
|
||||
|
||||
private TypedKey<string> GetYtCacheKey(string query)
|
||||
=> new($"search:youtube:{query}");
|
||||
|
||||
private async Task AddYoutubeUrlToCacheAsync(string query, string url)
|
||||
=> await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours());
|
||||
|
||||
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
|
||||
{
|
||||
var result = await _cache.GetAsync(GetYtCacheKey(query));
|
||||
|
||||
if (!result.TryGetValue(out var url) || string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
return new VideoInfo()
|
||||
{
|
||||
Url = url
|
||||
};
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Youtube([Leftover] string? query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
await ErrorLocalizedAsync(strs.specify_search_params);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var maybeResult = await GetYoutubeUrlFromCacheAsync(query)
|
||||
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
|
||||
if (maybeResult is not {} result || result is {Url: null})
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.no_results);
|
||||
return;
|
||||
}
|
||||
|
||||
await AddYoutubeUrlToCacheAsync(query, result.Url);
|
||||
await ctx.Channel.SendMessageAsync(result.Url);
|
||||
}
|
||||
|
||||
// [Cmd]
|
||||
// public async Task DuckDuckGo([Leftover] string query = null)
|
||||
// {
|
||||
// query = query?.Trim();
|
||||
// if (!await ValidateQuery(query))
|
||||
// return;
|
||||
//
|
||||
// _ = ctx.Channel.TriggerTypingAsync();
|
||||
//
|
||||
// var data = await _service.DuckDuckGoSearchAsync(query);
|
||||
// if (data is null)
|
||||
// {
|
||||
// await ReplyErrorLocalizedAsync(strs.no_results);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var desc = data.Results.Take(5)
|
||||
// .Select(res => $@"[**{res.Title}**]({res.Link})
|
||||
// {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
|
||||
//
|
||||
// var descStr = string.Join("\n\n", desc);
|
||||
//
|
||||
// var embed = _eb.Create()
|
||||
// .WithAuthor(ctx.User.ToString(),
|
||||
// "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
|
||||
// .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
|
||||
// .WithOkColor();
|
||||
//
|
||||
// await ctx.Channel.EmbedAsync(embed);
|
||||
// }
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public abstract class SearchServiceBase : ISearchService
|
||||
{
|
||||
public abstract ITask<ISearchResult?> SearchAsync(string? query);
|
||||
public abstract ITask<IImageSearchResult?> SearchImagesAsync(string query);
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxImageSearchResult : IImageSearchResult
|
||||
{
|
||||
public string SearchTime { get; set; } = null!;
|
||||
|
||||
public ISearchResultInformation Info
|
||||
=> new SearxSearchResultInformation()
|
||||
{
|
||||
SearchTime = SearchTime,
|
||||
TotalResults = NumberOfResults.ToString("N", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
public IReadOnlyCollection<IImageSearchResultEntry> Entries
|
||||
=> Results;
|
||||
|
||||
[JsonPropertyName("results")]
|
||||
public List<SearxImageSearchResultEntry> Results { get; set; } = new List<SearxImageSearchResultEntry>();
|
||||
|
||||
[JsonPropertyName("query")]
|
||||
public string Query { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("number_of_results")]
|
||||
public double NumberOfResults { get; set; }
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxImageSearchResultEntry : IImageSearchResultEntry
|
||||
{
|
||||
public string Link
|
||||
=> ImageSource.StartsWith("//")
|
||||
? "https:" + ImageSource
|
||||
: ImageSource;
|
||||
|
||||
[JsonPropertyName("img_src")]
|
||||
public string ImageSource { get; set; } = string.Empty;
|
||||
}
|
30
src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs
Normal file
30
src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// using System.Text.Json.Serialization;
|
||||
//
|
||||
// namespace NadekoBot.Modules.Searches;
|
||||
//
|
||||
// public sealed class SearxInfobox
|
||||
// {
|
||||
// [JsonPropertyName("infobox")]
|
||||
// public string Infobox { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("id")]
|
||||
// public string Id { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("content")]
|
||||
// public string Content { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("img_src")]
|
||||
// public string ImgSrc { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("urls")]
|
||||
// public List<SearxUrlData> Urls { get; } = new List<SearxUrlData>();
|
||||
//
|
||||
// [JsonPropertyName("engine")]
|
||||
// public string Engine { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("engines")]
|
||||
// public List<string> Engines { get; } = new List<string>();
|
||||
//
|
||||
// [JsonPropertyName("attributes")]
|
||||
// public List<SearxSearchAttribute> Attributes { get; } = new List<SearxSearchAttribute>();
|
||||
// }
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxSearchAttribute
|
||||
{
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string? Value { get; set; }
|
||||
|
||||
[JsonPropertyName("entity")]
|
||||
public string? Entity { get; set; }
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxSearchResult : ISearchResult
|
||||
{
|
||||
[JsonPropertyName("query")]
|
||||
public string Query { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("number_of_results")]
|
||||
public double NumberOfResults { get; set; }
|
||||
|
||||
[JsonPropertyName("results")]
|
||||
public List<SearxSearchResultEntry> Results { get; set; } = new List<SearxSearchResultEntry>();
|
||||
|
||||
[JsonPropertyName("answers")]
|
||||
public List<string> Answers { get; set; } = new List<string>();
|
||||
//
|
||||
// [JsonPropertyName("corrections")]
|
||||
// public List<object> Corrections { get; } = new List<object>();
|
||||
|
||||
// [JsonPropertyName("infoboxes")]
|
||||
// public List<InfoboxModel> Infoboxes { get; } = new List<InfoboxModel>();
|
||||
//
|
||||
// [JsonPropertyName("suggestions")]
|
||||
// public List<string> Suggestions { get; } = new List<string>();
|
||||
|
||||
// [JsonPropertyName("unresponsive_engines")]
|
||||
// public List<object> UnresponsiveEngines { get; } = new List<object>();
|
||||
|
||||
|
||||
public string SearchTime { get; set; } = null!;
|
||||
|
||||
public IReadOnlyCollection<ISearchResultEntry> Entries
|
||||
=> Results;
|
||||
|
||||
public ISearchResultInformation Info
|
||||
=> new SearxSearchResultInformation()
|
||||
{
|
||||
SearchTime = SearchTime,
|
||||
TotalResults = NumberOfResults.ToString("N", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
public string? Answer
|
||||
=> Answers.FirstOrDefault();
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxSearchResultEntry : ISearchResultEntry
|
||||
{
|
||||
public string DisplayUrl
|
||||
=> Url;
|
||||
|
||||
public string Description
|
||||
=> Content.TrimTo(768)!;
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
// [JsonPropertyName("engine")]
|
||||
// public string Engine { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("parsed_url")]
|
||||
// public List<string> ParsedUrl { get; } = new List<string>();
|
||||
//
|
||||
// [JsonPropertyName("template")]
|
||||
// public string Template { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("engines")]
|
||||
// public List<string> Engines { get; } = new List<string>();
|
||||
//
|
||||
// [JsonPropertyName("positions")]
|
||||
// public List<int> Positions { get; } = new List<int>();
|
||||
//
|
||||
// [JsonPropertyName("score")]
|
||||
// public double Score { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("category")]
|
||||
// public string Category { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("pretty_url")]
|
||||
// public string PrettyUrl { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("open_group")]
|
||||
// public bool OpenGroup { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("close_group")]
|
||||
// public bool? CloseGroup { get; set; }
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxSearchResultInformation : ISearchResultInformation
|
||||
{
|
||||
public string TotalResults { get; init; } = string.Empty;
|
||||
public string SearchTime { get; init; } = string.Empty;
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
using MorseCode.ITask;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
{
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly SearchesConfigService _scs;
|
||||
|
||||
private static readonly Random _rng = new NadekoRandom();
|
||||
|
||||
public SearxSearchService(IHttpClientFactory http, SearchesConfigService scs)
|
||||
=> (_http, _scs) = (http, scs);
|
||||
|
||||
private string GetRandomInstance()
|
||||
{
|
||||
var instances = _scs.Data.SearxInstances;
|
||||
|
||||
if (instances is null or { Count: 0 })
|
||||
throw new InvalidOperationException("No searx instances specified in searches.yml");
|
||||
|
||||
return instances[_rng.Next(0, instances.Count)];
|
||||
}
|
||||
|
||||
public override async ITask<SearxSearchResult> SearchAsync(string? query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for web search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
+ $"&format=json"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
|
||||
public override async ITask<SearxImageSearchResult> SearchImagesAsync(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for img search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
+ $"&format=json"
|
||||
+ $"&category_images=on"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxImageSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
}
|
15
src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs
Normal file
15
src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class SearxUrlData
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("official")]
|
||||
public bool? Official { get; set; }
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public interface IYoutubeSearchService
|
||||
{
|
||||
Task<VideoInfo?> SearchAsync(string query);
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class InvidiousSearchResponse
|
||||
{
|
||||
[JsonPropertyName("videoId")]
|
||||
public string VideoId { get; set; } = null!;
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
using NadekoBot.Modules.Searches.Youtube;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
|
||||
{
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly SearchesConfigService _scs;
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
public InvidiousYtSearchService(
|
||||
IHttpClientFactory http,
|
||||
SearchesConfigService scs)
|
||||
{
|
||||
_http = http;
|
||||
_scs = scs;
|
||||
_rng = new();
|
||||
}
|
||||
|
||||
public async Task<VideoInfo?> SearchAsync(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var instances = _scs.Data.InvidiousInstances;
|
||||
if (instances is null or { Count: 0 })
|
||||
{
|
||||
Log.Warning("Attempted to use Invidious as the .youtube provider but there are no 'invidiousInstances' "
|
||||
+ "specified in `data/searches.yml`");
|
||||
return null;
|
||||
}
|
||||
|
||||
var instance = instances[_rng.Next(0, instances.Count)];
|
||||
|
||||
using var http = _http.CreateClient();
|
||||
var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
|
||||
$"{instance}/api/v1/search"
|
||||
+ $"?q={query}"
|
||||
+ $"&type=video");
|
||||
|
||||
if (res is null or {Count: 0})
|
||||
return null;
|
||||
|
||||
return new VideoInfo(res[0].VideoId);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public readonly struct VideoInfo
|
||||
{
|
||||
public VideoInfo(string videoId)
|
||||
=> Url = $"https://youtube.com/watch?v={videoId}";
|
||||
|
||||
public string Url { get; init; }
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INService
|
||||
{
|
||||
private readonly IGoogleApiService _gapi;
|
||||
|
||||
public YoutubeDataApiSearchService(IGoogleApiService gapi)
|
||||
{
|
||||
_gapi = gapi;
|
||||
}
|
||||
|
||||
public async Task<VideoInfo?> SearchAsync(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var results = await _gapi.GetVideoLinksByKeywordAsync(query);
|
||||
var first = results.FirstOrDefault();
|
||||
if (first is null)
|
||||
return null;
|
||||
|
||||
return new()
|
||||
{
|
||||
Url = first
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, INService
|
||||
{
|
||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
||||
=> await InternalGetInfoAsync(query, false);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, INService
|
||||
{
|
||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
||||
=> await InternalGetInfoAsync(query, true);
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
namespace NadekoBot.Modules.Searches.Youtube;
|
||||
|
||||
public abstract class YoutubedlxServiceBase : IYoutubeSearchService
|
||||
{
|
||||
private YtdlOperation CreateYtdlOp(bool isYtDlp)
|
||||
=> new YtdlOperation("-4 "
|
||||
+ "--geo-bypass "
|
||||
+ "--encoding UTF8 "
|
||||
+ "--get-id "
|
||||
+ "--no-check-certificate "
|
||||
+ "--default-search "
|
||||
+ "\"ytsearch:\" -- \"{0}\"",
|
||||
isYtDlp: isYtDlp);
|
||||
|
||||
protected async Task<VideoInfo?> InternalGetInfoAsync(string query, bool isYtDlp)
|
||||
{
|
||||
var op = CreateYtdlOp(isYtDlp);
|
||||
var data = await op.GetDataAsync(query);
|
||||
var items = data?.Split('\n');
|
||||
if (items is null or { Length: 0 })
|
||||
return null;
|
||||
|
||||
var id = items.FirstOrDefault(x => x.Length is > 5 and < 15);
|
||||
if (id is null)
|
||||
return null;
|
||||
|
||||
return new VideoInfo()
|
||||
{
|
||||
Url = $"https://youtube.com/watch?v={id}"
|
||||
};
|
||||
}
|
||||
|
||||
public abstract Task<VideoInfo?> SearchAsync(string query);
|
||||
}
|
Reference in New Issue
Block a user