This repository has been archived on 2024-12-22. You can view files and clone it, but cannot push or open issues or pull requests.
elliebot/src/EllieBot/Modules/Searches/SearchesService.cs

620 lines
22 KiB
C#
Raw Normal View History

2024-06-18 11:55:36 +00:00
#nullable disable
using EllieBot.Modules.Searches.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
2024-07-29 06:49:57 +00:00
using OneOf;
using System.Text.Json;
2024-06-18 11:55:36 +00:00
namespace EllieBot.Modules.Searches.Services;
public class SearchesService : IEService
{
public enum ImageTag
{
Food,
Dogs,
Cats,
Birds
}
public List<WoWJoke> WowJokes { get; } = [];
public List<MagicItem> MagicItems { get; } = [];
private readonly IHttpClientFactory _httpFactory;
private readonly IGoogleApiService _google;
private readonly IImageCache _imgs;
private readonly IBotCache _c;
private readonly FontProvider _fonts;
private readonly IBotCredsProvider _creds;
private readonly EllieRandom _rng;
private readonly List<string> _yomamaJokes;
private readonly object _yomamaLock = new();
private int yomamaJokeIndex;
private readonly ConcurrentDictionary<string, string> _cachedShortenedLinks = new();
2024-06-18 11:55:36 +00:00
public SearchesService(
IGoogleApiService google,
IImageCache images,
IBotCache c,
IHttpClientFactory factory,
FontProvider fonts,
IBotCredsProvider creds)
{
_httpFactory = factory;
_google = google;
_imgs = images;
_c = c;
_fonts = fonts;
_creds = creds;
_rng = new();
//joke commands
if (File.Exists("data/wowjokes.json"))
WowJokes = JsonConvert.DeserializeObject<List<WoWJoke>>(File.ReadAllText("data/wowjokes.json"));
else
Log.Warning("data/wowjokes.json is missing. WOW Jokes are not loaded");
if (File.Exists("data/magicitems.json"))
MagicItems = JsonConvert.DeserializeObject<List<MagicItem>>(File.ReadAllText("data/magicitems.json"));
else
Log.Warning("data/magicitems.json is missing. Magic items are not loaded");
if (File.Exists("data/yomama.txt"))
_yomamaJokes = File.ReadAllLines("data/yomama.txt").Shuffle().ToList();
else
{
_yomamaJokes = [];
Log.Warning("data/yomama.txt is missing. .yomama command won't work");
}
}
public async Task<WeatherData> GetWeatherDataAsync(string query)
{
query = query.Trim().ToLowerInvariant();
return await _c.GetOrAddAsync(new($"ellie_weather_{query}"),
2024-06-18 11:55:36 +00:00
async () => await GetWeatherDataFactory(query),
TimeSpan.FromHours(3));
}
private async Task<WeatherData> GetWeatherDataFactory(string query)
{
using var http = _httpFactory.CreateClient();
try
{
var data = await http.GetStringAsync("https://api.openweathermap.org/data/2.5/weather?"
+ $"q={query}&"
+ "appid=42cd627dd60debf25a5739e50a217d74&"
+ "units=metric");
if (string.IsNullOrWhiteSpace(data))
return null;
return JsonConvert.DeserializeObject<WeatherData>(data);
}
catch (Exception ex)
{
Log.Warning(ex, "Error getting weather data");
return null;
}
}
2024-07-29 06:38:07 +00:00
public Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataAsync(string arg)
2024-06-18 11:55:36 +00:00
=> GetTimeDataFactory(arg);
//return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}",
2024-06-18 11:55:36 +00:00
// GetTimeDataFactory,
// arg,
// TimeSpan.FromMinutes(1));
2024-07-29 06:38:07 +00:00
private async Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataFactory(
2024-06-18 11:55:36 +00:00
string query)
{
query = query.Trim();
if (string.IsNullOrEmpty(query))
2024-07-29 06:38:07 +00:00
return (default, ErrorType.InvalidInput);
2024-06-18 11:55:36 +00:00
var locIqKey = _creds.GetCreds().LocationIqApiKey;
var tzDbKey = _creds.GetCreds().TimezoneDbApiKey;
if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey))
2024-07-29 06:38:07 +00:00
return (default, ErrorType.ApiKeyMissing);
2024-06-18 11:55:36 +00:00
try
{
using var http = _httpFactory.CreateClient();
var res = await _c.GetOrAddAsync(new($"searches:geo:{query}"),
async () =>
{
var url = "https://eu1.locationiq.com/v1/search.php?"
+ (string.IsNullOrWhiteSpace(locIqKey)
? "key="
: $"key={locIqKey}&")
+ $"q={Uri.EscapeDataString(query)}&"
+ "format=json";
var res = await http.GetStringAsync(url);
return res;
},
TimeSpan.FromHours(1));
var responses = JsonConvert.DeserializeObject<LocationIqResponse[]>(res);
if (responses is null || responses.Length == 0)
{
Log.Warning("Geocode lookup failed for: {Query}", query);
2024-07-29 06:38:07 +00:00
return (default, ErrorType.NotFound);
2024-06-18 11:55:36 +00:00
}
var geoData = responses[0];
using var req = new HttpRequestMessage(HttpMethod.Get,
"http://api.timezonedb.com/v2.1/get-time-zone?"
+ $"key={tzDbKey}"
+ $"&format=json"
+ $"&by=position"
+ $"&lat={geoData.Lat}"
+ $"&lng={geoData.Lon}");
using var geoRes = await http.SendAsync(req);
var resString = await geoRes.Content.ReadAsStringAsync();
var timeObj = JsonConvert.DeserializeObject<TimeZoneResult>(resString);
var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timeObj.Timestamp);
return ((Address: responses[0].DisplayName, Time: time, TimeZoneName: timeObj.TimezoneName), default);
}
catch (Exception ex)
{
Log.Error(ex, "Weather error: {Message}", ex.Message);
2024-07-29 06:38:07 +00:00
return (default, ErrorType.NotFound);
2024-06-18 11:55:36 +00:00
}
}
public string GetRandomImageUrl(ImageTag tag)
{
var subpath = tag.ToString().ToLowerInvariant();
var max = tag switch
{
ImageTag.Food => 773,
ImageTag.Dogs => 750,
ImageTag.Cats => 773,
ImageTag.Birds => 578,
_ => 100,
};
return $"https://nadeko-pictures.nyc3.digitaloceanspaces.com/{subpath}/"
+ _rng.Next(1, max).ToString("000")
+ ".png";
}
public Task<string> GetYomamaJoke()
{
string joke;
lock (_yomamaLock)
{
if (yomamaJokeIndex >= _yomamaJokes.Count)
{
yomamaJokeIndex = 0;
var newList = _yomamaJokes.ToList();
_yomamaJokes.Clear();
_yomamaJokes.AddRange(newList.Shuffle());
}
joke = _yomamaJokes[yomamaJokeIndex++];
}
return Task.FromResult(joke);
// using (var http = _httpFactory.CreateClient())
// {
// var response = await http.GetStringAsync(new Uri("http://api.yomomma.info/"));
// return JObject.Parse(response)["joke"].ToString() + " 😆";
// }
}
public async Task<(string Setup, string Punchline)> GetRandomJoke()
{
using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync("https://official-joke-api.appspot.com/random_joke");
var resObj = JsonConvert.DeserializeAnonymousType(res,
new
{
setup = "",
punchline = ""
});
return (resObj.setup, resObj.punchline);
}
public async Task<string> GetChuckNorrisJoke()
{
using var http = _httpFactory.CreateClient();
var response = await http.GetStringAsync(new Uri("https://api.chucknorris.io/jokes/random"));
return JObject.Parse(response)["value"] + " 😆";
}
public async Task<MtgData> GetMtgCardAsync(string search)
{
search = search.Trim().ToLowerInvariant();
var data = await _c.GetOrAddAsync(new($"mtg:{search}"),
async () => await GetMtgCardFactory(search),
TimeSpan.FromDays(1));
if (data is null || data.Length == 0)
return null;
return data[_rng.Next(0, data.Length)];
}
private async Task<MtgData[]> GetMtgCardFactory(string search)
{
async Task<MtgData> GetMtgDataAsync(MtgResponse.Data card)
{
string storeUrl;
try
{
storeUrl = await _google.ShortenUrl("https://shop.tcgplayer.com/productcatalog/product/show?"
+ "newSearch=false&"
+ "ProductType=All&"
+ "IsProductNameExact=false&"
+ $"ProductName={Uri.EscapeDataString(card.Name)}");
}
catch { storeUrl = "<url can't be found>"; }
return new()
{
Description = card.Text,
Name = card.Name,
ImageUrl = card.ImageUrl,
StoreUrl = storeUrl,
Types = string.Join(",\n", card.Types),
ManaCost = card.ManaCost
};
}
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
var response =
await http.GetStringAsync($"https://api.magicthegathering.io/v1/cards?name={Uri.EscapeDataString(search)}");
var responseObject = JsonConvert.DeserializeObject<MtgResponse>(response);
if (responseObject is null)
return Array.Empty<MtgData>();
var cards = responseObject.Cards.Take(5).ToArray();
if (cards.Length == 0)
return Array.Empty<MtgData>();
return await cards.Select(GetMtgDataAsync).WhenAll();
}
public async Task<HearthstoneCardData> GetHearthstoneCardDataAsync(string name)
{
name = name.ToLowerInvariant();
return await _c.GetOrAddAsync($"hearthstone:{name}",
() => HearthstoneCardDataFactory(name),
TimeSpan.FromDays(1));
}
private async Task<HearthstoneCardData> HearthstoneCardDataFactory(string name)
{
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.GetCreds().RapidApiKey);
try
{
var response = await http.GetStringAsync("https://omgvamp-hearthstone-v1.p.rapidapi.com/"
+ $"cards/search/{Uri.EscapeDataString(name)}");
var objs = JsonConvert.DeserializeObject<HearthstoneCardData[]>(response);
if (objs is null || objs.Length == 0)
return null;
var data = objs.FirstOrDefault(x => x.Collectible)
?? objs.FirstOrDefault(x => !string.IsNullOrEmpty(x.PlayerClass)) ?? objs.FirstOrDefault();
if (data is null)
return null;
if (!string.IsNullOrWhiteSpace(data.Img))
data.Img = await _google.ShortenUrl(data.Img);
// if (!string.IsNullOrWhiteSpace(data.Text))
// {
// var converter = new Converter();
// data.Text = converter.Convert(data.Text);
// }
return data;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Hearthstone Card: {ErrorMessage}", ex.Message);
return null;
}
}
public async Task<OmdbMovie> GetMovieDataAsync(string name)
{
name = name.Trim().ToLowerInvariant();
return await _c.GetOrAddAsync(new($"movie:{name}"),
() => GetMovieDataFactory(name),
TimeSpan.FromDays(1));
}
private async Task<OmdbMovie> GetMovieDataFactory(string name)
{
using var http = _httpFactory.CreateClient();
2024-07-29 06:32:34 +00:00
var res = await http.GetStringAsync("https://omdbapi.nadeko.bot/"
+ $"?t={name.Trim().Replace(' ', '+')}"
+ "&y="
+ "&plot=full"
+ "&r=json");
2024-06-18 11:55:36 +00:00
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
if (movie?.Title is null)
return null;
movie.Poster = await _google.ShortenUrl(movie.Poster);
return movie;
}
public async Task<int> GetSteamAppIdByName(string query)
{
const string steamGameIdsKey = "steam_names_to_appid";
var gamesMap = await _c.GetOrAddAsync(new(steamGameIdsKey),
async () =>
{
using var http = _httpFactory.CreateClient();
// https://api.steampowered.com/ISteamApps/GetAppList/v2/
var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/");
var apps = JsonConvert
.DeserializeAnonymousType(gamesStr,
new
{
applist = new
{
apps = new List<SteamGameId>()
}
})!
.applist.apps;
return apps.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.GroupBy(x => x.Name)
.ToDictionary(x => x.Key, x => x.First().AppId);
},
TimeSpan.FromHours(24));
if (gamesMap is null)
return -1;
query = query.Trim();
var keyList = gamesMap.Keys.ToList();
var key = keyList.FirstOrDefault(x => x.Equals(query, StringComparison.OrdinalIgnoreCase));
if (key == default)
{
key = keyList.FirstOrDefault(x => x.StartsWith(query, StringComparison.OrdinalIgnoreCase));
if (key == default)
return -1;
}
return gamesMap[key];
}
2024-07-29 06:26:18 +00:00
public async Task<OneOf<WikipediaReply, ErrorType>> GetWikipediaPageAsync(string query)
2024-07-29 06:26:18 +00:00
{
query = query.Trim();
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
try
{
var result = await _c.GetOrAddAsync($"wikipedia_{query}",
2024-07-29 06:32:34 +00:00
async () =>
2024-07-29 06:26:18 +00:00
{
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
return await http.GetStringAsync(
"https://en.wikipedia.org/w/api.php?action=query"
+ "&format=json"
+ "&prop=info"
+ "&redirects=1"
+ "&formatversion=2"
+ "&inprop=url"
+ "&titles="
+ Uri.EscapeDataString(query));
},
TimeSpan.FromHours(1))
.ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
if (data.Query.Pages is null || !data.Query.Pages.Any() || data.Query.Pages.First().Missing)
{
return ErrorType.NotFound;
}
Log.Information("Sending wikipedia url for: {Query}", query);
return new WikipediaReply
{
Data = new()
{
Url = data.Query.Pages[0].FullUrl,
}
};
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving wikipedia data for: '{Query}'", query);
return ErrorType.Unknown;
}
}
2024-07-29 06:49:57 +00:00
public async Task<OneOf<string, ErrorType>> GetCatFactAsync()
{
using var http = _httpFactory.CreateClient();
var response = await http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false);
var doc = JsonDocument.Parse(response);
2024-07-31 13:03:08 +00:00
2024-07-29 06:49:57 +00:00
if (!doc.RootElement.TryGetProperty("fact", out var factElement))
{
return ErrorType.Unknown;
}
return factElement.ToString();
}
2024-07-29 06:26:18 +00:00
2024-07-31 13:03:08 +00:00
public async Task<OneOf<WikiaResponse, ErrorType>> GetWikiaPageAsync(string target, string query)
2024-07-29 06:26:18 +00:00
{
2024-07-31 13:03:08 +00:00
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query))
{
return ErrorType.InvalidInput;
}
2024-07-29 06:26:18 +00:00
2024-07-31 13:03:08 +00:00
query = Uri.EscapeDataString(query.Trim());
target = Uri.EscapeDataString(target.Trim());
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
try
{
var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php"
+ "?action=query"
+ "&format=json"
+ "&list=search"
+ $"&srsearch={Uri.EscapeDataString(query)}"
+ "&srlimit=1");
var items = JObject.Parse(res);
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
if (string.IsNullOrWhiteSpace(title))
{
return ErrorType.NotFound;
}
var url = $"https://{target}.fandom.com/wiki/{title}";
return new WikiaResponse()
{
Url = url,
Title = title,
};
}
catch (Exception ex)
{
Log.Warning(ex, "Error getting wikia page: {Message}", ex.Message);
return ErrorType.Unknown;
}
}
private static TypedKey<string> GetDefineKey(string query)
=> new TypedKey<string>($"define_{query}");
public async Task<OneOf<List<DefineData>, ErrorType>> GetDefinitionsAsync(string query)
{
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
query = Uri.EscapeDataString(query);
using var http = _httpFactory.CreateClient();
string res;
try
{
res = await _c.GetOrAddAsync(GetDefineKey(query),
async () => await http.GetStringAsync(
$"https://api.pearson.com/v2/dictionaries/entries?headword={query}"),
TimeSpan.FromHours(12));
var responseModel = JsonConvert.DeserializeObject<DefineModel>(res);
var data = responseModel.Results
.Where(x => x.Senses is not null
&& x.Senses.Count > 0
&& x.Senses[0].Definition is not null)
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
.ToList();
if (!data.Any())
{
Log.Warning("Definition not found: {Word}", query);
return ErrorType.NotFound;
}
var items = new List<DefineData>();
foreach (var d in data)
{
items.Add(new DefineData
{
Definition = d.Sense.Definition is JArray { Count: > 0 } defs
? defs[0].ToString()
: d.Sense.Definition.ToString(),
Example = d.Sense.Examples is null || d.Sense.Examples.Count == 0
? string.Empty
: d.Sense.Examples[0].Text,
WordType = string.IsNullOrWhiteSpace(d.PartOfSpeech) ? "-" : d.PartOfSpeech,
Word = query,
});
}
return items.OrderByDescending(x => !string.IsNullOrWhiteSpace(x.Example)).ToList();
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving definition data for: {Word}", query);
return ErrorType.Unknown;
}
}
public async Task<string> ShortenLink(string query)
{
query = query.Trim();
if (_cachedShortenedLinks.TryGetValue(query, out var shortLink))
return shortLink;
try
{
using var http = _httpFactory.CreateClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://goolnk.com/api/v1/shorten");
var formData = new MultipartFormDataContent
{
{ new StringContent(query), "url" }
};
req.Content = formData;
using var res = await http.SendAsync(req);
var content = await res.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<ShortenData>(content);
if (!string.IsNullOrWhiteSpace(data?.ResultUrl))
_cachedShortenedLinks.TryAdd(query, data.ResultUrl);
else
return query;
shortLink = data.ResultUrl;
}
catch (Exception ex)
{
Log.Error(ex, "Error shortening a link: {Message}", ex.Message);
return null;
}
return shortLink;
throw new NotImplementedException();
}
2024-06-18 11:55:36 +00:00
}