Updated Searches module

This commit is contained in:
Toastie (DCS Team) 2024-06-26 23:51:35 +12:00
parent 888994dd67
commit 0466701e28
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
8 changed files with 77 additions and 383 deletions

View file

@ -130,7 +130,7 @@ public class CryptoService : IEService
await _getCryptoLock.WaitAsync();
try
{
var data = await _cache.GetOrAddAsync(new("ellie:crypto_data"),
var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"),
async () =>
{
try

View file

@ -1,6 +1,7 @@
using AngleSharp;
using CsvHelper;
using CsvHelper.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
@ -22,46 +23,32 @@ public sealed class DefaultStockDataService : IStockDataService, IEService
using var http = _httpClientFactory.CreateClient();
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}";
var config = Configuration.Default.WithDefaultLoader();
using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage);
var divElem =
document.QuerySelector(
"#quote-header-info > div:nth-child(2) > div > div > h1");
var tickerName = (divElem)?.TextContent;
var tickerName = document.QuerySelector("div.top > .left > .container > h1")
?.TextContent;
if (tickerName is null)
return default;
var marketcap = document
.QuerySelectorAll("table")
.Skip(1)
.First()
.QuerySelector("tbody > tr > td:nth-child(2)")
.QuerySelector("li > span > fin-streamer[data-field='marketCap']")
?.TextContent;
var volume = document.QuerySelector("td[data-test='AVERAGE_VOLUME_3MONTH-value']")
var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']")
?.TextContent;
var close= document.QuerySelector("td[data-test='PREV_CLOSE-value']")
?.TextContent ?? "0";
var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']")
?.TextContent
?? "0";
var price = document
.QuerySelector("#quote-header-info")
?.QuerySelector("fin-streamer[data-field='regularMarketPrice']")
?.TextContent ?? close;
// var data = await http.GetFromJsonAsync<YahooQueryModel>(
// $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={query}");
//
// if (data is null)
// return default;
// var symbol = data.QuoteResponse.Result.FirstOrDefault();
// if (symbol is null)
// return default;
var price = document.QuerySelector("fin-streamer.livePrice > span")
?.TextContent
?? "0";
return new()
{

View file

@ -1,5 +1,4 @@
#nullable disable
using CodeHollow.FeedReader;
using CodeHollow.FeedReader;
using EllieBot.Modules.Searches.Services;
using System.Text.RegularExpressions;
@ -17,19 +16,21 @@ public partial class Searches
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public Task YtUploadNotif(string url, [Leftover] string message = null)
public Task YtUploadNotif(string url, [Leftover] string? message = null)
=> YtUploadNotif(url, null, message);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(2)]
public Task YtUploadNotif(string url, ITextChannel channel = null, [Leftover] string message = null)
public Task YtUploadNotif(string url, ITextChannel? channel = null, [Leftover] string? message = null)
{
var m = _ytChannelRegex.Match(url);
if (!m.Success)
return Response().Error(strs.invalid_input).SendAsync();
channel ??= ctx.Channel as ITextChannel;
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
message = message?.SanitizeAllMentions();
@ -42,7 +43,7 @@ public partial class Searches
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Feed(string url, [Leftover] string message = null)
public Task Feed(string url, [Leftover] string? message = null)
=> Feed(url, null, message);
@ -50,7 +51,7 @@ public partial class Searches
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task Feed(string url, ITextChannel channel = null, [Leftover] string message = null)
public async Task Feed(string url, ITextChannel? channel = null, [Leftover] string? message = null)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
@ -59,10 +60,11 @@ public partial class Searches
return;
}
channel ??= (ITextChannel)ctx.Channel;
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
message = message?.SanitizeAllMentions();
channel ??= (ITextChannel)ctx.Channel;
try
{
await FeedReader.ReadAsync(url);

View file

@ -79,6 +79,15 @@ public class FeedsService : IEService
}
}
private DateTime? GetPubDate(FeedItem item)
{
if (item.PublishingDate is not null)
return item.PublishingDate;
if (item.SpecificItem is AtomFeedItem atomItem)
return atomItem.UpdatedDate;
return null;
}
public async Task<EmbedBuilder> TrackFeeds()
{
while (true)
@ -94,24 +103,32 @@ public class FeedsService : IEService
{
var feed = await FeedReader.ReadAsync(rssUrl);
var items = feed
.Items.Select(item => (Item: item,
LastUpdate: item.PublishingDate?.ToUniversalTime()
?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate?.ToUniversalTime()))
.Where(data => data.LastUpdate is not null)
.Select(data => (data.Item, LastUpdate: (DateTime)data.LastUpdate))
.OrderByDescending(data => data.LastUpdate)
.Reverse() // start from the oldest
.ToList();
var items = new List<(FeedItem Item, DateTime LastUpdate)>();
foreach (var item in feed.Items)
{
var pubDate = GetPubDate(item);
if (pubDate is null)
continue;
items.Add((item, pubDate.Value.ToUniversalTime()));
// show at most 3 items if you're behind
if (items.Count > 2)
break;
}
if (items.Count == 0)
continue;
if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate))
{
lastFeedUpdate = _lastPosts[kvp.Key] =
items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow;
lastFeedUpdate = _lastPosts[kvp.Key] = items[0].LastUpdate;
}
foreach (var (feedItem, itemUpdateDate) in items)
for (var index = 1; index <= items.Count; index++)
{
var (feedItem, itemUpdateDate) = items[^index];
if (itemUpdateDate <= lastFeedUpdate)
continue;
@ -168,27 +185,26 @@ public class FeedsService : IEService
if (!string.IsNullOrWhiteSpace(feedItem.Description))
embed.WithDescription(desc.TrimTo(2048));
//send the created embed to all subscribed channels
var feedSendTasks = kvp.Value
.Where(x => x.GuildConfig is not null)
.Select(x =>
var tasks = new List<Task>();
foreach (var val in kvp.Value)
{
var ch = _client.GetGuild(x.GuildConfig.GuildId)
?.GetTextChannel(x.ChannelId);
var ch = _client.GetGuild(val.GuildConfig.GuildId).GetTextChannel(val.ChannelId);
if (ch is null)
return null;
continue;
return _sender.Response(ch)
var sendTask = _sender.Response(ch)
.Embed(embed)
.Text(string.IsNullOrWhiteSpace(x.Message)
.Text(string.IsNullOrWhiteSpace(val.Message)
? string.Empty
: x.Message)
: val.Message)
.SendAsync();
})
.Where(x => x is not null);
tasks.Add(sendTask);
}
allSendTasks.Add(feedSendTasks.WhenAll());
allSendTasks.Add(tasks.WhenAll());
// as data retrieval was successful, reset error counter
ClearErrors(rssUrl);

View file

@ -1,312 +0,0 @@
#nullable disable
using EllieBot.Modules.Searches.Common;
using EllieBot.Modules.Searches.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Text;
namespace EllieBot.Modules.Searches;
public partial class Searches
{
[Group]
public partial class PathOfExileCommands : EllieModule<SearchesService>
{
private const string POE_URL = "https://www.pathofexile.com/character-window/get-characters?accountName=";
private const string PON_URL = "http://poe.ninja/api/Data/GetCurrencyOverview?league=";
private const string POGS_URL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search=";
private const string POG_URL =
"https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject=";
private const string POGI_URL =
"https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:";
private const string PROFILE_URL = "https://www.pathofexile.com/account/view-profile/";
private readonly IHttpClientFactory _httpFactory;
private Dictionary<string, string> currencyDictionary = new(StringComparer.OrdinalIgnoreCase)
{
{ "Chaos Orb", "Chaos Orb" },
{ "Orb of Alchemy", "Orb of Alchemy" },
{ "Jeweller's Orb", "Jeweller's Orb" },
{ "Exalted Orb", "Exalted Orb" },
{ "Mirror of Kalandra", "Mirror of Kalandra" },
{ "Vaal Orb", "Vaal Orb" },
{ "Orb of Alteration", "Orb of Alteration" },
{ "Orb of Scouring", "Orb of Scouring" },
{ "Divine Orb", "Divine Orb" },
{ "Orb of Annulment", "Orb of Annulment" },
{ "Master Cartographer's Sextant", "Master Cartographer's Sextant" },
{ "Journeyman Cartographer's Sextant", "Journeyman Cartographer's Sextant" },
{ "Apprentice Cartographer's Sextant", "Apprentice Cartographer's Sextant" },
{ "Blessed Orb", "Blessed Orb" },
{ "Orb of Regret", "Orb of Regret" },
{ "Gemcutter's Prism", "Gemcutter's Prism" },
{ "Glassblower's Bauble", "Glassblower's Bauble" },
{ "Orb of Fusing", "Orb of Fusing" },
{ "Cartographer's Chisel", "Cartographer's Chisel" },
{ "Chromatic Orb", "Chromatic Orb" },
{ "Orb of Augmentation", "Orb of Augmentation" },
{ "Blacksmith's Whetstone", "Blacksmith's Whetstone" },
{ "Orb of Transmutation", "Orb of Transmutation" },
{ "Armourer's Scrap", "Armourer's Scrap" },
{ "Scroll of Wisdom", "Scroll of Wisdom" },
{ "Regal Orb", "Regal Orb" },
{ "Chaos", "Chaos Orb" },
{ "Alch", "Orb of Alchemy" },
{ "Alchs", "Orb of Alchemy" },
{ "Jews", "Jeweller's Orb" },
{ "Jeweller", "Jeweller's Orb" },
{ "Jewellers", "Jeweller's Orb" },
{ "Jeweller's", "Jeweller's Orb" },
{ "X", "Exalted Orb" },
{ "Ex", "Exalted Orb" },
{ "Exalt", "Exalted Orb" },
{ "Exalts", "Exalted Orb" },
{ "Mirror", "Mirror of Kalandra" },
{ "Mirrors", "Mirror of Kalandra" },
{ "Vaal", "Vaal Orb" },
{ "Alt", "Orb of Alteration" },
{ "Alts", "Orb of Alteration" },
{ "Scour", "Orb of Scouring" },
{ "Scours", "Orb of Scouring" },
{ "Divine", "Divine Orb" },
{ "Annul", "Orb of Annulment" },
{ "Annulment", "Orb of Annulment" },
{ "Master Sextant", "Master Cartographer's Sextant" },
{ "Journeyman Sextant", "Journeyman Cartographer's Sextant" },
{ "Apprentice Sextant", "Apprentice Cartographer's Sextant" },
{ "Blessed", "Blessed Orb" },
{ "Regret", "Orb of Regret" },
{ "Regrets", "Orb of Regret" },
{ "Gcp", "Gemcutter's Prism" },
{ "Glassblowers", "Glassblower's Bauble" },
{ "Glassblower's", "Glassblower's Bauble" },
{ "Fusing", "Orb of Fusing" },
{ "Fuses", "Orb of Fusing" },
{ "Fuse", "Orb of Fusing" },
{ "Chisel", "Cartographer's Chisel" },
{ "Chisels", "Cartographer's Chisel" },
{ "Chance", "Orb of Chance" },
{ "Chances", "Orb of Chance" },
{ "Chrome", "Chromatic Orb" },
{ "Chromes", "Chromatic Orb" },
{ "Aug", "Orb of Augmentation" },
{ "Augmentation", "Orb of Augmentation" },
{ "Augment", "Orb of Augmentation" },
{ "Augments", "Orb of Augmentation" },
{ "Whetstone", "Blacksmith's Whetstone" },
{ "Whetstones", "Blacksmith's Whetstone" },
{ "Transmute", "Orb of Transmutation" },
{ "Transmutes", "Orb of Transmutation" },
{ "Armourers", "Armourer's Scrap" },
{ "Armourer's", "Armourer's Scrap" },
{ "Wisdom Scroll", "Scroll of Wisdom" },
{ "Wisdom Scrolls", "Scroll of Wisdom" },
{ "Regal", "Regal Orb" },
{ "Regals", "Regal Orb" }
};
public PathOfExileCommands(IHttpClientFactory httpFactory)
=> _httpFactory = httpFactory;
[Cmd]
public async Task PathOfExile(string usr, string league = "", int page = 1)
{
if (--page < 0)
return;
if (string.IsNullOrWhiteSpace(usr))
{
await Response().Error("Please provide an account name.").SendAsync();
return;
}
var characters = new List<Account>();
try
{
using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync($"{POE_URL}{usr}");
characters = JsonConvert.DeserializeObject<List<Account>>(res);
}
catch
{
var embed = _sender.CreateEmbed().WithDescription(GetText(strs.account_not_found)).WithErrorColor();
await Response().Embed(embed).SendAsync();
return;
}
if (!string.IsNullOrWhiteSpace(league))
characters.RemoveAll(c => c.League != league);
await Response()
.Paginated()
.Items(characters)
.PageSize(9)
.CurrentPage(page)
.Page((items, curPage) =>
{
var embed = _sender.CreateEmbed()
.WithAuthor($"Characters on {usr}'s account",
"https://web.poecdn.com/image/favicon/ogimage.png",
$"{PROFILE_URL}{usr}")
.WithOkColor();
if (characters.Count == 0)
return embed.WithDescription("This account has no characters.");
var sb = new StringBuilder();
sb.AppendLine($"```{"#",-5}{"Character Name",-23}{"League",-10}{"Class",-13}{"Level",-3}");
for (var i = 0; i < items.Count; i++)
{
var character = items[i];
sb.AppendLine(
$"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}");
}
sb.AppendLine("```");
embed.WithDescription(sb.ToString());
return embed;
})
.SendAsync();
}
[Cmd]
public async Task PathOfExileLeagues()
{
var leagues = new List<Leagues>();
try
{
using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync("http://api.pathofexile.com/leagues?type=main&compact=1");
leagues = JsonConvert.DeserializeObject<List<Leagues>>(res);
}
catch
{
var eembed = _sender.CreateEmbed().WithDescription(GetText(strs.leagues_not_found)).WithErrorColor();
await Response().Embed(eembed).SendAsync();
return;
}
var embed = _sender.CreateEmbed()
.WithAuthor("Path of Exile Leagues",
"https://web.poecdn.com/image/favicon/ogimage.png",
"https://www.pathofexile.com")
.WithOkColor();
var sb = new StringBuilder();
sb.AppendLine($"```{"#",-5}{"League Name",-23}");
for (var i = 0; i < leagues.Count; i++)
{
var league = leagues[i];
sb.AppendLine($"#{i + 1,-4}{league.Id,-23}");
}
sb.AppendLine("```");
embed.WithDescription(sb.ToString());
await Response().Embed(embed).SendAsync();
}
[Cmd]
public async Task PathOfExileCurrency(
string leagueName,
string currencyName,
string convertName = "Chaos Orb")
{
if (string.IsNullOrWhiteSpace(leagueName))
{
await Response().Error("Please provide league name.").SendAsync();
return;
}
if (string.IsNullOrWhiteSpace(currencyName))
{
await Response().Error("Please provide currency name.").SendAsync();
return;
}
var cleanCurrency = ShortCurrencyName(currencyName);
var cleanConvert = ShortCurrencyName(convertName);
try
{
var res = $"{PON_URL}{leagueName}";
using var http = _httpFactory.CreateClient();
var obj = JObject.Parse(await http.GetStringAsync(res));
var chaosEquivalent = 0.0F;
var conversionEquivalent = 0.0F;
// poe.ninja API does not include a "chaosEquivalent" property for Chaos Orbs.
if (cleanCurrency == "Chaos Orb")
chaosEquivalent = 1.0F;
else
{
var currencyInput = obj["lines"]
.Values<JObject>()
.Where(i => i["currencyTypeName"].Value<string>() == cleanCurrency)
.FirstOrDefault();
chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(),
CultureInfo.InvariantCulture);
}
if (cleanConvert == "Chaos Orb")
conversionEquivalent = 1.0F;
else
{
var currencyOutput = obj["lines"]
.Values<JObject>()
.Where(i => i["currencyTypeName"].Value<string>() == cleanConvert)
.FirstOrDefault();
conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(),
CultureInfo.InvariantCulture);
}
var embed = _sender.CreateEmbed()
.WithAuthor($"{leagueName} Currency Exchange",
"https://web.poecdn.com/image/favicon/ogimage.png",
"http://poe.ninja")
.AddField("Currency Type", cleanCurrency, true)
.AddField($"{cleanConvert} Equivalent", chaosEquivalent / conversionEquivalent, true)
.WithOkColor();
await Response().Embed(embed).SendAsync();
}
catch
{
var embed = _sender.CreateEmbed().WithDescription(GetText(strs.ninja_not_found)).WithErrorColor();
await Response().Embed(embed).SendAsync();
}
}
private string ShortCurrencyName(string str)
{
if (currencyDictionary.ContainsValue(str))
return str;
var currency = currencyDictionary[str];
return currency;
}
private static string ShortLeagueName(string str)
{
var league = str.Replace("Hardcore", "HC", StringComparison.InvariantCultureIgnoreCase);
return league;
}
}
}

View file

@ -2,6 +2,7 @@
using Microsoft.Extensions.Caching.Memory;
using EllieBot.Modules.Searches.Common;
using EllieBot.Modules.Searches.Services;
using EllieBot.Modules.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SixLabors.ImageSharp;
@ -168,7 +169,7 @@ public partial class Searches : EllieModule<SearchesService>
.AddField("Rating", movie.ImdbRating, true)
.AddField("Genre", movie.Genre, true)
.AddField("Year", movie.Year, true)
.WithImageUrl(movie.Poster))
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null))
.SendAsync();
}

View file

@ -126,7 +126,7 @@ public class SearchesService : IEService
{
query = query.Trim().ToLowerInvariant();
return await _c.GetOrAddAsync(new($"ellie_weather_{query}"),
return await _c.GetOrAddAsync(new($"nadeko_weather_{query}"),
async () => await GetWeatherDataFactory(query),
TimeSpan.FromHours(3));
}
@ -156,7 +156,7 @@ public class SearchesService : IEService
public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg)
=> GetTimeDataFactory(arg);
//return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}",
//return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}",
// GetTimeDataFactory,
// arg,
// TimeSpan.FromMinutes(1));

View file

@ -40,7 +40,7 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
[Comment("""
Set the searx instance urls in case you want to use 'searx' for either img or web search.
Ellie will use a random one for each request.
Nadeko will use a random one for each request.
Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`
Instances specified must support 'format=json' query parameter.
- In case you're running your own searx instance, set
@ -57,7 +57,7 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
[Comment("""
Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search
Ellie will use a random one for each request.
Nadeko will use a random one for each request.
These instances may be used for music queue functionality in the future.
Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com