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
2024-06-26 23:51:35 +12:00

457 lines
No EOL
16 KiB
C#

#nullable disable
using EllieBot.Modules.Searches.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
using Image = SixLabors.ImageSharp.Image;
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;
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<Stream> GetRipPictureAsync(string text, Uri imgUrl)
=> (await GetRipPictureFactory(text, imgUrl)).ToStream();
private void DrawAvatar(Image bg, Image avatarImage)
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions()));
public async Task<byte[]> GetRipPictureFactory(string text, Uri avatarUrl)
{
using var bg = Image.Load<Rgba32>(await _imgs.GetRipBgAsync());
var result = await _c.GetImageDataAsync(avatarUrl);
if (!result.TryPickT0(out var data, out _))
{
using var http = _httpFactory.CreateClient();
data = await http.GetByteArrayAsync(avatarUrl);
using (var avatarImg = Image.Load<Rgba32>(data))
{
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
await using var avStream = await avatarImg.ToStreamAsync();
data = avStream.ToArray();
DrawAvatar(bg, avatarImg);
}
await _c.SetImageDataAsync(avatarUrl, data);
}
else
{
using var avatarImg = Image.Load<Rgba32>(data);
DrawAvatar(bg, avatarImg);
}
bg.Mutate(x => x.DrawText(
new TextOptions(_fonts.RipFont)
{
HorizontalAlignment = HorizontalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(bg.Width / 2, 225),
},
text,
Color.Black));
//flowa
using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync()))
{
bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions()));
}
await using var stream = bg.ToStream();
return stream.ToArray();
}
public async Task<WeatherData> GetWeatherDataAsync(string query)
{
query = query.Trim().ToLowerInvariant();
return await _c.GetOrAddAsync(new($"nadeko_weather_{query}"),
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;
}
}
public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg)
=> GetTimeDataFactory(arg);
//return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}",
// GetTimeDataFactory,
// arg,
// TimeSpan.FromMinutes(1));
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory(
string query)
{
query = query.Trim();
if (string.IsNullOrEmpty(query))
return (default, TimeErrors.InvalidInput);
var locIqKey = _creds.GetCreds().LocationIqApiKey;
var tzDbKey = _creds.GetCreds().TimezoneDbApiKey;
if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey))
return (default, TimeErrors.ApiKeyMissing);
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);
return (default, TimeErrors.NotFound);
}
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);
return (default, TimeErrors.NotFound);
}
}
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();
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/"
+ "?t={0}"
+ "&y="
+ "&plot=full"
+ "&r=json",
name.Trim().Replace(' ', '+')));
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];
}
}