Compare commits

..

No commits in common. "f9b21520fbb6937a33097c83f303d81533d2fb64" and "a4adabb3ea3853cbec6bfda4543a3f8e9a491ffa" have entirely different histories.

50 changed files with 467 additions and 566 deletions

View file

@ -1,7 +1,6 @@
#nullable disable #nullable disable
using EllieBot.Modules.Music.Services; using EllieBot.Modules.Music.Services;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Music; namespace EllieBot.Modules.Music;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class MusicPlaylist : DbEntity public class MusicPlaylist : DbEntity

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Modules.Permissions.Services; using EllieBot.Modules.Permissions.Services;
using EllieBot.Db.Models; using EllieBot.Db.Models;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Permissions.Services; namespace EllieBot.Modules.Permissions.Services;
public readonly struct ServerFilterSettings public readonly struct ServerFilterSettings

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
namespace EllieBot.Modules.Permissions.Services; namespace EllieBot.Modules.Permissions.Services;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Modules.Permissions.Common; namespace EllieBot.Modules.Permissions.Common;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Modules.Permissions.Common; namespace EllieBot.Modules.Permissions.Common;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Permissions.Common; namespace EllieBot.Modules.Permissions.Common;
public class PermissionsCollection<T> : IndexedCollection<T> public class PermissionsCollection<T> : IndexedCollection<T>

View file

@ -149,7 +149,8 @@ public class PermissionService : IExecPreCommand, IEService
returnMsg = "You need Admin permissions in order to use permission commands."; returnMsg = "You need Admin permissions in order to use permission commands.";
if (pc.Verbose) if (pc.Verbose)
{ {
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); } try
{ await _sender.Response(channel).Error(returnMsg).SendAsync(); }
catch { } catch { }
} }
@ -161,7 +162,8 @@ public class PermissionService : IExecPreCommand, IEService
returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands."; returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands.";
if (pc.Verbose) if (pc.Verbose)
{ {
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); } try
{ await _sender.Response(channel).Error(returnMsg).SendAsync(); }
catch { } catch { }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,314 +0,0 @@
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Administration;
using EllieBot.Modules.Games.Services;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace EllieBot.Modules.Utility;
public enum GetCommandErrorResult
{
RateLimitHit,
NotAuthorized,
Disregard,
Unknown
}
public sealed class AiAssistantService
: IAiAssistantService, IReadyExecutor,
IExecOnMessage,
IEService
{
private IReadOnlyCollection<AiCommandModel> _commands = [];
private readonly IBotStrings _strings;
private readonly IHttpClientFactory _httpFactory;
private readonly CommandService _cmds;
private readonly IBotCredsProvider _credsProvider;
private readonly DiscordSocketClient _client;
private readonly ICommandHandler _cmdHandler;
private readonly BotConfigService _bcs;
private readonly IMessageSenderService _sender;
private readonly JsonSerializerOptions _serializerOptions = new();
private readonly IPermissionChecker _permChecker;
private readonly IBotCache _botCache;
private readonly ChatterBotService _cbs;
public AiAssistantService(
DiscordSocketClient client,
IBotStrings strings,
IHttpClientFactory httpFactory,
CommandService cmds,
IBotCredsProvider credsProvider,
ICommandHandler cmdHandler,
BotConfigService bcs,
IPermissionChecker permChecker,
IBotCache botCache,
ChatterBotService cbs,
IMessageSenderService sender)
{
_client = client;
_strings = strings;
_httpFactory = httpFactory;
_cmds = cmds;
_credsProvider = credsProvider;
_cmdHandler = cmdHandler;
_bcs = bcs;
_sender = sender;
_permChecker = permChecker;
_botCache = botCache;
_cbs = cbs;
}
public async Task<OneOf.OneOf<EllieCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
ulong userId,
string prompt,
IReadOnlyCollection<AiCommandModel> commands,
string prefix)
{
using var content = new StringContent(
JsonSerializer.Serialize(new
{
query = prompt,
commands = commands.ToDictionary(x => x.Name,
x => new AiCommandModel()
{
Desc = string.Format(x.Desc ?? "", prefix),
Params = x.Params,
Name = x.Name
}),
}),
Encoding.UTF8,
"application/json"
);
using var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
// request.RequestUri = new("https://nai.nadeko.bot/get-command");
request.RequestUri = new("https://nai.nadeko.bot/get-command");
request.Content = content;
var creds = _credsProvider.GetCreds();
request.Headers.TryAddWithoutValidation("x-auth-token", creds.EllieAiToken);
request.Headers.TryAddWithoutValidation("x-auth-userid", userId.ToString());
using var client = _httpFactory.CreateClient();
// todo customize according to the bot's config
// - CurrencyName
// -
using var response = await client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
return GetCommandErrorResult.RateLimitHit;
}
else if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return GetCommandErrorResult.NotAuthorized;
}
var funcModel = await response.Content.ReadFromJsonAsync<CommandPromptResultModel>();
if (funcModel?.Name == "disregard")
{
Log.Warning("Disregarding the prompt: {Prompt}", prompt);
return GetCommandErrorResult.Disregard;
}
if (funcModel is null)
return GetCommandErrorResult.Unknown;
var comModel = new EllieCommandCallModel()
{
Name = funcModel.Name,
Arguments = funcModel.Arguments
.OrderBy(param => _commands.FirstOrDefault(x => x.Name == funcModel.Name)
?.Params
.Select((x, i) => (x, i))
.Where(x => x.x.Name == param.Key)
.Select(x => x.i)
.FirstOrDefault())
.Select(x => x.Value)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToArray(),
Remaining = funcModel.Remaining
};
return comModel;
}
public IReadOnlyCollection<AiCommandModel> GetCommands()
=> _commands;
public Task OnReadyAsync()
{
var cmds = _cmds.Commands
.Select(x => (MethodName: x.Summary, CommandName: x.Aliases[0]))
.Where(x => !x.MethodName.Contains("///"))
.Distinct()
.ToList();
var funcs = new List<AiCommandModel>();
foreach (var (method, cmd) in cmds)
{
var commandStrings = _strings.GetCommandStrings(method);
if (commandStrings is null)
continue;
funcs.Add(new()
{
Name = cmd,
Desc = commandStrings?.Desc?.Replace("currency", "flowers") ?? string.Empty,
Params = commandStrings?.Params.FirstOrDefault()
?.Select(x => new AiCommandParamModel()
{
Desc = x.Value.Desc,
Name = x.Key,
})
.ToArray()
?? []
});
}
_commands = funcs;
return Task.CompletedTask;
}
public int Priority
=> 2;
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
{
if (string.IsNullOrWhiteSpace(_credsProvider.GetCreds().EllieAiToken))
return false;
if (guild is not SocketGuild sg)
return false;
var nadekoId = _client.CurrentUser.Id;
var channel = msg.Channel as ITextChannel;
if (channel is null)
return false;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string query;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
query = msg.Content[normalMention.Length..].Trim();
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
query = msg.Content[nickMention.Length..].Trim();
else
return false;
var success = await TryExecuteAiCommand(guild, msg, channel, query);
return success;
}
public async Task<bool> TryExecuteAiCommand(
IGuild guild,
IUserMessage msg,
ITextChannel channel,
string query)
{
// check permissions
var pcResult = await _permChecker.CheckPermsAsync(
guild,
msg.Channel,
msg.Author,
"Utility",
"prompt"
);
if (!pcResult.IsAllowed)
return false;
using var _ = channel.EnterTypingState();
var result = await TryGetCommandAsync(msg.Author.Id, query, _commands, _cmdHandler.GetPrefix(guild.Id));
if (result.TryPickT0(out var model, out var error))
{
if (model.Name == ".ai_chat")
{
if (guild is not SocketGuild sg)
return false;
var sess = _cbs.GetOrCreateSession(guild.Id);
if (sess is null)
return false;
await _cbs.RunChatterBot(sg, msg, channel, sess, query);
return false;
}
var commandString = GetCommandString(model);
var msgTask = _sender.Response(channel)
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithAuthor(msg.Author.GlobalName,
msg.Author.RealAvatarUrl().ToString())
.WithDescription(commandString))
.SendAsync();
await _cmdHandler.TryRunCommand(
(SocketGuild)guild,
(ISocketMessageChannel)channel,
new DoAsUserMessage((SocketUserMessage)msg, msg.Author, commandString));
var cmdMsg = await msgTask;
cmdMsg.DeleteAfter(5);
return true;
}
if (error == GetCommandErrorResult.Disregard)
{
// await msg.ErrorAsync();
return false;
}
var key = new TypedKey<bool>($"sub_error:{msg.Author.Id}:{error}");
if (!await _botCache.AddAsync(key, true, TimeSpan.FromDays(1), overwrite: false))
return false;
var errorMsg = error switch
{
GetCommandErrorResult.RateLimitHit
=> "You've spent your daily requests quota.",
GetCommandErrorResult.NotAuthorized
=> "In order to use this command you have to have a 5$ or higher subscription at <https://patreon.com/nadekobot>",
GetCommandErrorResult.Unknown
=> "The service is temporarily unavailable.",
_ => throw new ArgumentOutOfRangeException()
};
await _sender.Response(channel)
.Error(errorMsg)
.SendAsync();
return true;
}
private string GetCommandString(EllieCommandCallModel res)
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
private static string GetParamString(string val, bool isLast)
=> isLast ? val : "\"" + val + "\"";
}

View file

@ -1,15 +0,0 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class AiCommandModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("desc")]
public required string Desc { get; set; }
[JsonPropertyName("params")]
public required IReadOnlyList<AiCommandParamModel> Params { get; set; }
}

View file

@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class AiCommandParamModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("desc")]
public required string Desc { get; set; }
}

View file

@ -1,16 +0,0 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Utility;
public sealed class CommandPromptResultModel
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("arguments")]
public required Dictionary<string, string> Arguments { get; set; }
[JsonPropertyName("remaining")]
[JsonConverter(typeof(NumberToStringConverter))]
public required string Remaining { get; set; }
}

View file

@ -1,8 +0,0 @@
namespace EllieBot.Modules.Utility;
public sealed class EllieCommandCallModel
{
public required string Name { get; set; }
public required IReadOnlyList<string> Arguments { get; set; }
public required string Remaining { get; set; }
}

View file

@ -1,20 +0,0 @@
using OneOf;
namespace EllieBot.Modules.Utility;
public interface IAiAssistantService
{
Task<OneOf<EllieCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
ulong userId,
string prompt,
IReadOnlyCollection<AiCommandModel> commands,
string prefix);
IReadOnlyCollection<AiCommandModel> GetCommands();
Task<bool> TryExecuteAiCommand(
IGuild guild,
IUserMessage msg,
ITextChannel channel,
string query);
}

View file

@ -1,23 +0,0 @@
using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Utility;
public partial class UtilityCommands
{
public class PromptCommands : EllieModule<IAiAssistantService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Prompt([Leftover] string query)
{
await ctx.Channel.TriggerTypingAsync();
var res = await _service.TryExecuteAiCommand(ctx.Guild, ctx.Message, (ITextChannel)ctx.Channel, query);
}
private string GetCommandString(EllieCommandCallModel res)
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
private static string GetParamString(string val, bool isLast)
=> isLast ? val : "\"" + val + "\"";
}
}

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Utility; namespace EllieBot.Modules.Utility;
public partial class Utility public partial class Utility

View file

@ -50,7 +50,7 @@ public partial class Utility
{ {
var success = await _service.EndGiveawayAsync(ctx.Guild.Id, id); var success = await _service.EndGiveawayAsync(ctx.Guild.Id, id);
if(!success) if (!success)
{ {
await Response().Error(strs.giveaway_not_found).SendAsync(); await Response().Error(strs.giveaway_not_found).SendAsync();
return; return;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using System.Text; using System.Text;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
@ -144,9 +144,9 @@ public partial class Utility
true) true)
.WithOkColor(); .WithOkColor();
var mPatron = await _ps.GetPatronAsync(user.Id); var patron = await _ps.GetPatronAsync(user.Id);
if (mPatron is {} patron && patron.Tier != PatronTier.None) if (patron.Tier != PatronTier.None)
{ {
embed.WithFooter(patron.Tier switch embed.WithFooter(patron.Tier switch
{ {

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using CommandLine; using CommandLine;
namespace EllieBot.Modules.Utility.Services; namespace EllieBot.Modules.Utility.Services;

View file

@ -1,4 +1,4 @@
#nullable disable warnings #nullable disable warnings
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.Yml; using EllieBot.Common.Yml;
@ -158,7 +158,7 @@ public partial class Utility
{ {
var msg = sm.Data.Components.FirstOrDefault()?.Value; var msg = sm.Data.Components.FirstOrDefault()?.Value;
if(!string.IsNullOrWhiteSpace(msg)) if (!string.IsNullOrWhiteSpace(msg))
await QuoteEdit(id, msg); await QuoteEdit(id, msg);
} }
); );

View file

@ -1,4 +1,4 @@
#nullable disable warnings #nullable disable warnings
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;

View file

@ -113,9 +113,10 @@ public partial class Utility
foreach (var rem in rems) foreach (var rem in rems)
{ {
var when = rem.When; var when = rem.When;
var diff = when - DateTime.UtcNow;
embed.AddField( embed.AddField(
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC " $"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC "
+ $"{TimestampTag.FromDateTime(when)}", + $"(in {diff.ToPrettyStringHm()})",
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")} $@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
`TargetId:` {rem.ChannelId} `TargetId:` {rem.ChannelId}
`Message:` {rem.Message?.TrimTo(50)}"); `Message:` {rem.Message?.TrimTo(50)}");
@ -202,15 +203,16 @@ public partial class Utility
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
// var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id)); var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
try try
{ {
await Response() await Response()
.Confirm($"\u23f0 {GetText(strs.remind2( .Confirm($"\u23f0 {GetText(strs.remind(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username), Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
Format.Bold(message), Format.Bold(message),
TimestampTag.FromDateTime(DateTime.UtcNow.Add(ts), TimestampTagStyles.Relative), ts.ToPrettyStringHm(),
TimestampTag.FormatFromDateTime(time, TimestampTagStyles.ShortDateTime)))}") gTime,
gTime))}")
.SendAsync(); .SendAsync();
} }
catch catch

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Modules.Utility.Services; namespace EllieBot.Modules.Utility.Services;

View file

@ -72,7 +72,7 @@ public class ConverterService : IEService, IReadyExecutor
var stream = File.OpenRead("data/units.json"); var stream = File.OpenRead("data/units.json");
var defaultUnits = await JsonSerializer.DeserializeAsync<ConvertUnit[]>(stream); var defaultUnits = await JsonSerializer.DeserializeAsync<ConvertUnit[]>(stream);
if(defaultUnits is not null) if (defaultUnits is not null)
units.AddRange(defaultUnits); units.AddRange(defaultUnits);
units.Add(baseType); units.Add(baseType);

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using EllieBot.Modules.Utility.Services; using EllieBot.Modules.Utility.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Diagnostics; using System.Diagnostics;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
using System.Diagnostics; using System.Diagnostics;
namespace EllieBot.Modules.Utility.Common; namespace EllieBot.Modules.Utility.Common;

View file

@ -1,4 +1,4 @@
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
#nullable disable #nullable disable
namespace EllieBot.Modules.Utility; namespace EllieBot.Modules.Utility;

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Utility.Common.Exceptions; namespace EllieBot.Modules.Utility.Common.Exceptions;
public class StreamRoleNotFoundException : Exception public class StreamRoleNotFoundException : Exception

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Utility.Common.Exceptions; namespace EllieBot.Modules.Utility.Common.Exceptions;
public class StreamRolePermissionException : Exception public class StreamRolePermissionException : Exception

View file

@ -1,4 +1,4 @@
#nullable disable #nullable disable
namespace EllieBot.Modules.Utility.Common; namespace EllieBot.Modules.Utility.Common;
public enum StreamRoleListType public enum StreamRoleListType