mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 17:58:26 -04:00
Applied codestyle to all .cs files
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#nullable disable
|
||||
using AngleSharp;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using AngleSharp.Html.Dom;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
@@ -38,7 +38,8 @@ public partial class Searches
|
||||
// await ctx.Channel.EmbedAsync(embed);
|
||||
// }
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[Priority(0)]
|
||||
public async Task Mal([Leftover] string name)
|
||||
{
|
||||
@@ -49,59 +50,64 @@ public partial class Searches
|
||||
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
using var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink);
|
||||
var imageElem = document.QuerySelector("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img");
|
||||
var imageUrl = ((IHtmlImageElement)imageElem)?.Source ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png";
|
||||
var imageElem =
|
||||
document.QuerySelector(
|
||||
"body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img");
|
||||
var imageUrl = ((IHtmlImageElement)imageElem)?.Source
|
||||
?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png";
|
||||
|
||||
var stats = document.QuerySelectorAll("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span").Select(x => x.InnerHtml).ToList();
|
||||
var stats = document
|
||||
.QuerySelectorAll(
|
||||
"body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span")
|
||||
.Select(x => x.InnerHtml)
|
||||
.ToList();
|
||||
|
||||
var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc");
|
||||
|
||||
var favAnime = GetText(strs.anime_no_fav);
|
||||
if (favorites.Length > 0 && favorites[0].QuerySelector("p") is null)
|
||||
favAnime = string.Join("\n", favorites[0].QuerySelectorAll("ul > li > div.di-tc.va-t > a")
|
||||
.Shuffle()
|
||||
.Take(3)
|
||||
.Select(x =>
|
||||
{
|
||||
var elem = (IHtmlAnchorElement)x;
|
||||
return $"[{elem.InnerHtml}]({elem.Href})";
|
||||
}));
|
||||
favAnime = string.Join("\n",
|
||||
favorites[0]
|
||||
.QuerySelectorAll("ul > li > div.di-tc.va-t > a")
|
||||
.Shuffle()
|
||||
.Take(3)
|
||||
.Select(x =>
|
||||
{
|
||||
var elem = (IHtmlAnchorElement)x;
|
||||
return $"[{elem.InnerHtml}]({elem.Href})";
|
||||
}));
|
||||
|
||||
var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix")
|
||||
.Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml))
|
||||
.ToList();
|
||||
.Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml))
|
||||
.ToList();
|
||||
|
||||
var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div")
|
||||
.Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray())
|
||||
.ToArray();
|
||||
.Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray())
|
||||
.ToArray();
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.mal_profile(name)))
|
||||
.AddField("💚 " + GetText(strs.watching), stats[0], true)
|
||||
.AddField("💙 " + GetText(strs.completed), stats[1], true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.mal_profile(name)))
|
||||
.AddField("💚 " + GetText(strs.watching), stats[0], true)
|
||||
.AddField("💙 " + GetText(strs.completed), stats[1], true);
|
||||
if (info.Count < 3)
|
||||
embed.AddField("💛 " + GetText(strs.on_hold), stats[2], true);
|
||||
embed
|
||||
.AddField("💔 " + GetText(strs.dropped), stats[3], true)
|
||||
.AddField("⚪ " + GetText(strs.plan_to_watch), stats[4], true)
|
||||
.AddField("🕐 " + daysAndMean[0][0], daysAndMean[0][1], true)
|
||||
.AddField("📊 " + daysAndMean[1][0], daysAndMean[1][1], true)
|
||||
.AddField(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1, info[0].Item2.TrimTo(20), true)
|
||||
.AddField(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1, info[1].Item2.TrimTo(20), true);
|
||||
embed.AddField("💔 " + GetText(strs.dropped), stats[3], true)
|
||||
.AddField("⚪ " + GetText(strs.plan_to_watch), stats[4], true)
|
||||
.AddField("🕐 " + daysAndMean[0][0], daysAndMean[0][1], true)
|
||||
.AddField("📊 " + daysAndMean[1][0], daysAndMean[1][1], true)
|
||||
.AddField(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1, info[0].Item2.TrimTo(20), true)
|
||||
.AddField(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1, info[1].Item2.TrimTo(20), true);
|
||||
if (info.Count > 2)
|
||||
embed.AddField(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1, info[2].Item2.TrimTo(20), true);
|
||||
|
||||
embed
|
||||
.WithDescription($@"
|
||||
** https://myanimelist.net/animelist/{ name } **
|
||||
embed.WithDescription($@"
|
||||
** https://myanimelist.net/animelist/{name} **
|
||||
|
||||
**{GetText(strs.top_3_fav_anime)}**
|
||||
{favAnime}"
|
||||
|
||||
)
|
||||
.WithUrl(fullQueryLink)
|
||||
.WithImageUrl(imageUrl);
|
||||
{favAnime}")
|
||||
.WithUrl(fullQueryLink)
|
||||
.WithImageUrl(imageUrl);
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
@@ -124,12 +130,15 @@ public partial class Searches
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public Task Mal(IGuildUser usr) => Mal(usr.Username);
|
||||
public Task Mal(IGuildUser usr)
|
||||
=> Mal(usr.Username);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Anime([Leftover] string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
@@ -144,19 +153,24 @@ public partial class Searches
|
||||
}
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithDescription(animeData.Synopsis.Replace("<br>", Environment.NewLine, StringComparison.InvariantCulture))
|
||||
.WithTitle(animeData.TitleEnglish)
|
||||
.WithUrl(animeData.Link)
|
||||
.WithImageUrl(animeData.ImageUrlLarge)
|
||||
.AddField(GetText(strs.episodes), animeData.TotalEpisodes.ToString(), true)
|
||||
.AddField(GetText(strs.status), animeData.AiringStatus.ToString(), true)
|
||||
.AddField(GetText(strs.genres), string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" }), true)
|
||||
.WithFooter($"{GetText(strs.score)} {animeData.AverageScore} / 100");
|
||||
.WithOkColor()
|
||||
.WithDescription(animeData.Synopsis.Replace("<br>",
|
||||
Environment.NewLine,
|
||||
StringComparison.InvariantCulture))
|
||||
.WithTitle(animeData.TitleEnglish)
|
||||
.WithUrl(animeData.Link)
|
||||
.WithImageUrl(animeData.ImageUrlLarge)
|
||||
.AddField(GetText(strs.episodes), animeData.TotalEpisodes.ToString(), true)
|
||||
.AddField(GetText(strs.status), animeData.AiringStatus, true)
|
||||
.AddField(GetText(strs.genres),
|
||||
string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" }),
|
||||
true)
|
||||
.WithFooter($"{GetText(strs.score)} {animeData.AverageScore} / 100");
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Manga([Leftover] string query)
|
||||
{
|
||||
@@ -172,17 +186,21 @@ public partial class Searches
|
||||
}
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithDescription(mangaData.Synopsis.Replace("<br>", Environment.NewLine, StringComparison.InvariantCulture))
|
||||
.WithTitle(mangaData.TitleEnglish)
|
||||
.WithUrl(mangaData.Link)
|
||||
.WithImageUrl(mangaData.ImageUrlLge)
|
||||
.AddField(GetText(strs.chapters), mangaData.TotalChapters.ToString(), true)
|
||||
.AddField(GetText(strs.status), mangaData.PublishingStatus.ToString(), true)
|
||||
.AddField(GetText(strs.genres), string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" }), true)
|
||||
.WithFooter($"{GetText(strs.score)} {mangaData.AverageScore} / 100");
|
||||
.WithOkColor()
|
||||
.WithDescription(mangaData.Synopsis.Replace("<br>",
|
||||
Environment.NewLine,
|
||||
StringComparison.InvariantCulture))
|
||||
.WithTitle(mangaData.TitleEnglish)
|
||||
.WithUrl(mangaData.Link)
|
||||
.WithImageUrl(mangaData.ImageUrlLge)
|
||||
.AddField(GetText(strs.chapters), mangaData.TotalChapters.ToString(), true)
|
||||
.AddField(GetText(strs.status), mangaData.PublishingStatus, true)
|
||||
.AddField(GetText(strs.genres),
|
||||
string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" }),
|
||||
true)
|
||||
.WithFooter($"{GetText(strs.score)} {mangaData.AverageScore} / 100");
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,20 +6,32 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
public class AnimeResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string AiringStatus => AiringStatusParsed.ToTitleCase();
|
||||
|
||||
public string AiringStatus
|
||||
=> AiringStatusParsed.ToTitleCase();
|
||||
|
||||
[JsonProperty("airing_status")]
|
||||
public string AiringStatusParsed { get; set; }
|
||||
|
||||
[JsonProperty("title_english")]
|
||||
public string TitleEnglish { get; set; }
|
||||
|
||||
[JsonProperty("total_episodes")]
|
||||
public int TotalEpisodes { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonProperty("image_url_lge")]
|
||||
public string ImageUrlLarge { get; set; }
|
||||
|
||||
public string[] Genres { get; set; }
|
||||
|
||||
[JsonProperty("average_score")]
|
||||
public string AverageScore { get; set; }
|
||||
|
||||
public string Link => "http://anilist.co/anime/" + Id;
|
||||
public string Synopsis => Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "...";
|
||||
}
|
||||
public string Link
|
||||
=> "http://anilist.co/anime/" + Id;
|
||||
|
||||
public string Synopsis
|
||||
=> Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "...";
|
||||
}
|
@@ -13,7 +13,8 @@ public class BibleVerse
|
||||
{
|
||||
[JsonProperty("book_name")]
|
||||
public string BookName { get; set; }
|
||||
|
||||
public int Chapter { get; set; }
|
||||
public int Verse { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@ public class CryptoResponseData
|
||||
|
||||
[JsonProperty("cmc_rank")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
public CurrencyQuotes Quote { get; set; }
|
||||
}
|
||||
|
||||
@@ -33,4 +34,4 @@ public class Quote
|
||||
public string Percent_Change_24h { get; set; }
|
||||
public string Percent_Change_7d { get; set; }
|
||||
public double? Volume_24h { get; set; }
|
||||
}
|
||||
}
|
@@ -23,6 +23,7 @@ public class Sens
|
||||
{
|
||||
public object Definition { get; set; }
|
||||
public List<Example> Examples { get; set; }
|
||||
|
||||
[JsonProperty("gramatical_info")]
|
||||
public GramaticalInfo GramaticalInfo { get; set; }
|
||||
}
|
||||
@@ -31,6 +32,7 @@ public class Result
|
||||
{
|
||||
[JsonProperty("part_of_speech")]
|
||||
public string PartOfSpeech { get; set; }
|
||||
|
||||
public List<Sens> Senses { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
@@ -38,4 +40,4 @@ public class Result
|
||||
public class DefineModel
|
||||
{
|
||||
public List<Result> Results { get; set; }
|
||||
}
|
||||
}
|
@@ -3,6 +3,10 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class E621Object
|
||||
{
|
||||
public FileData File { get; set; }
|
||||
public TagData Tags { get; set; }
|
||||
public ScoreData Score { get; set; }
|
||||
|
||||
public class FileData
|
||||
{
|
||||
public string Url { get; set; }
|
||||
@@ -17,8 +21,4 @@ public class E621Object
|
||||
{
|
||||
public string Total { get; set; }
|
||||
}
|
||||
|
||||
public FileData File { get; set; }
|
||||
public TagData Tags { get; set; }
|
||||
public ScoreData Score { get; set; }
|
||||
}
|
||||
}
|
@@ -7,11 +7,13 @@ public class StreamNotFoundException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
public StreamNotFoundException(string message) : base(message)
|
||||
public StreamNotFoundException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamNotFoundException(string message, Exception innerException) : base(message, innerException)
|
||||
public StreamNotFoundException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -41,4 +41,4 @@ public sealed class Gallery
|
||||
UploadedAt = uploadedAt;
|
||||
Tags = tags;
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,34 +5,48 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class UserData
|
||||
{
|
||||
[JsonProperty("abbr")] public object Abbr { get; set; }
|
||||
[JsonProperty("abbr")]
|
||||
public object Abbr { get; set; }
|
||||
|
||||
[JsonProperty("clanid")] public object Clanid { get; set; }
|
||||
[JsonProperty("clanid")]
|
||||
public object Clanid { get; set; }
|
||||
|
||||
[JsonProperty("country")] public string Country { get; set; }
|
||||
[JsonProperty("country")]
|
||||
public string Country { get; set; }
|
||||
|
||||
[JsonProperty("favourite_mode")] public int FavouriteMode { get; set; }
|
||||
[JsonProperty("favourite_mode")]
|
||||
public int FavouriteMode { get; set; }
|
||||
|
||||
[JsonProperty("followers_count")] public int FollowersCount { get; set; }
|
||||
[JsonProperty("followers_count")]
|
||||
public int FollowersCount { get; set; }
|
||||
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("latest_activity")] public int LatestActivity { get; set; }
|
||||
[JsonProperty("latest_activity")]
|
||||
public int LatestActivity { get; set; }
|
||||
|
||||
[JsonProperty("play_style")] public int PlayStyle { get; set; }
|
||||
[JsonProperty("play_style")]
|
||||
public int PlayStyle { get; set; }
|
||||
|
||||
[JsonProperty("privileges")] public int Privileges { get; set; }
|
||||
[JsonProperty("privileges")]
|
||||
public int Privileges { get; set; }
|
||||
|
||||
[JsonProperty("registered_on")] public int RegisteredOn { get; set; }
|
||||
[JsonProperty("registered_on")]
|
||||
public int RegisteredOn { get; set; }
|
||||
|
||||
[JsonProperty("username")] public string Username { get; set; }
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonProperty("username_aka")] public string UsernameAka { get; set; }
|
||||
[JsonProperty("username_aka")]
|
||||
public string UsernameAka { get; set; }
|
||||
}
|
||||
|
||||
public class GatariUserResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("users")] public List<UserData> Users { get; set; }
|
||||
}
|
||||
[JsonProperty("users")]
|
||||
public List<UserData> Users { get; set; }
|
||||
}
|
@@ -5,50 +5,72 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class UserStats
|
||||
{
|
||||
[JsonProperty("a_count")] public int ACount { get; set; }
|
||||
[JsonProperty("a_count")]
|
||||
public int ACount { get; set; }
|
||||
|
||||
[JsonProperty("avg_accuracy")] public double AvgAccuracy { get; set; }
|
||||
[JsonProperty("avg_accuracy")]
|
||||
public double AvgAccuracy { get; set; }
|
||||
|
||||
[JsonProperty("avg_hits_play")] public double AvgHitsPlay { get; set; }
|
||||
[JsonProperty("avg_hits_play")]
|
||||
public double AvgHitsPlay { get; set; }
|
||||
|
||||
[JsonProperty("country_rank")] public int CountryRank { get; set; }
|
||||
[JsonProperty("country_rank")]
|
||||
public int CountryRank { get; set; }
|
||||
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("level")] public int Level { get; set; }
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
|
||||
[JsonProperty("level_progress")] public int LevelProgress { get; set; }
|
||||
[JsonProperty("level_progress")]
|
||||
public int LevelProgress { get; set; }
|
||||
|
||||
[JsonProperty("max_combo")] public int MaxCombo { get; set; }
|
||||
[JsonProperty("max_combo")]
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
[JsonProperty("playcount")] public int Playcount { get; set; }
|
||||
[JsonProperty("playcount")]
|
||||
public int Playcount { get; set; }
|
||||
|
||||
[JsonProperty("playtime")] public int Playtime { get; set; }
|
||||
[JsonProperty("playtime")]
|
||||
public int Playtime { get; set; }
|
||||
|
||||
[JsonProperty("pp")] public int Pp { get; set; }
|
||||
[JsonProperty("pp")]
|
||||
public int Pp { get; set; }
|
||||
|
||||
[JsonProperty("rank")] public int Rank { get; set; }
|
||||
[JsonProperty("rank")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
[JsonProperty("ranked_score")] public int RankedScore { get; set; }
|
||||
[JsonProperty("ranked_score")]
|
||||
public int RankedScore { get; set; }
|
||||
|
||||
[JsonProperty("replays_watched")] public int ReplaysWatched { get; set; }
|
||||
[JsonProperty("replays_watched")]
|
||||
public int ReplaysWatched { get; set; }
|
||||
|
||||
[JsonProperty("s_count")] public int SCount { get; set; }
|
||||
[JsonProperty("s_count")]
|
||||
public int SCount { get; set; }
|
||||
|
||||
[JsonProperty("sh_count")] public int ShCount { get; set; }
|
||||
[JsonProperty("sh_count")]
|
||||
public int ShCount { get; set; }
|
||||
|
||||
[JsonProperty("total_hits")] public int TotalHits { get; set; }
|
||||
[JsonProperty("total_hits")]
|
||||
public int TotalHits { get; set; }
|
||||
|
||||
[JsonProperty("total_score")] public long TotalScore { get; set; }
|
||||
[JsonProperty("total_score")]
|
||||
public long TotalScore { get; set; }
|
||||
|
||||
[JsonProperty("x_count")] public int XCount { get; set; }
|
||||
[JsonProperty("x_count")]
|
||||
public int XCount { get; set; }
|
||||
|
||||
[JsonProperty("xh_count")] public int XhCount { get; set; }
|
||||
[JsonProperty("xh_count")]
|
||||
public int XhCount { get; set; }
|
||||
}
|
||||
|
||||
public class GatariUserStatsResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("stats")] public UserStats Stats { get; set; }
|
||||
}
|
||||
[JsonProperty("stats")]
|
||||
public UserStats Stats { get; set; }
|
||||
}
|
@@ -9,8 +9,8 @@ public sealed class GoogleSearchResult
|
||||
|
||||
public GoogleSearchResult(string title, string link, string text)
|
||||
{
|
||||
this.Title = title;
|
||||
this.Link = link;
|
||||
this.Text = text;
|
||||
Title = title;
|
||||
Link = link;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,4 +10,4 @@ public class HearthstoneCardData
|
||||
public string Img { get; set; }
|
||||
public string ImgGold { get; set; }
|
||||
public string PlayerClass { get; set; }
|
||||
}
|
||||
}
|
@@ -13,24 +13,26 @@ public class ImageCacherObject : IComparable<ImageCacherObject>
|
||||
public ImageCacherObject(DapiImageObject obj, Booru type)
|
||||
{
|
||||
if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
|
||||
{
|
||||
this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
|
||||
}
|
||||
FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
|
||||
else
|
||||
{
|
||||
this.FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture) ? obj.FileUrl : "https:" + obj.FileUrl;
|
||||
}
|
||||
this.SearchType = type;
|
||||
this.Rating = obj.Rating;
|
||||
this.Tags = new((obj.Tags ?? obj.TagString).Split(' '));
|
||||
FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture)
|
||||
? obj.FileUrl
|
||||
: "https:" + obj.FileUrl;
|
||||
SearchType = type;
|
||||
Rating = obj.Rating;
|
||||
Tags = new((obj.Tags ?? obj.TagString).Split(' '));
|
||||
}
|
||||
|
||||
public ImageCacherObject(string url, Booru type, string tags, string rating)
|
||||
public ImageCacherObject(
|
||||
string url,
|
||||
Booru type,
|
||||
string tags,
|
||||
string rating)
|
||||
{
|
||||
this.SearchType = type;
|
||||
this.FileUrl = url;
|
||||
this.Tags = new(tags.Split(' '));
|
||||
this.Rating = rating;
|
||||
SearchType = type;
|
||||
FileUrl = url;
|
||||
Tags = new(tags.Split(' '));
|
||||
Rating = rating;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -38,4 +40,4 @@ public class ImageCacherObject : IComparable<ImageCacherObject>
|
||||
|
||||
public int CompareTo(ImageCacherObject other)
|
||||
=> string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ namespace SystemTextJsonSamples;
|
||||
public class LowerCaseNamingPolicy : JsonNamingPolicy
|
||||
{
|
||||
public static LowerCaseNamingPolicy Default = new();
|
||||
|
||||
public override string ConvertName(string name) =>
|
||||
name.ToLower();
|
||||
}
|
||||
|
||||
public override string ConvertName(string name)
|
||||
=> name.ToLower();
|
||||
}
|
@@ -5,4 +5,4 @@ public class MagicItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
@@ -6,20 +6,31 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
public class MangaResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("publishing_status")]
|
||||
public string PublishingStatus { get; set; }
|
||||
|
||||
[JsonProperty("image_url_lge")]
|
||||
public string ImageUrlLge { get; set; }
|
||||
|
||||
[JsonProperty("title_english")]
|
||||
public string TitleEnglish { get; set; }
|
||||
|
||||
[JsonProperty("total_chapters")]
|
||||
public int TotalChapters { get; set; }
|
||||
|
||||
[JsonProperty("total_volumes")]
|
||||
public int TotalVolumes { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
|
||||
[JsonProperty("average_score")]
|
||||
public string AverageScore { get; set; }
|
||||
public string Link => "http://anilist.co/manga/" + Id;
|
||||
public string Synopsis => Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "...";
|
||||
}
|
||||
|
||||
public string Link
|
||||
=> "http://anilist.co/manga/" + Id;
|
||||
|
||||
public string Synopsis
|
||||
=> Description?[..(Description.Length > 500 ? 500 : Description.Length)] + "...";
|
||||
}
|
@@ -13,6 +13,8 @@ public class MtgData
|
||||
|
||||
public class MtgResponse
|
||||
{
|
||||
public List<Data> Cards { get; set; }
|
||||
|
||||
public class Data
|
||||
{
|
||||
public string Name { get; set; }
|
||||
@@ -21,6 +23,4 @@ public class MtgResponse
|
||||
public List<string> Types { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
}
|
||||
|
||||
public List<Data> Cards { get; set; }
|
||||
}
|
||||
}
|
@@ -7,85 +7,115 @@ public static class NhentaiApiModel
|
||||
{
|
||||
public class Title
|
||||
{
|
||||
[JsonProperty("english")] public string English { get; set; }
|
||||
[JsonProperty("english")]
|
||||
public string English { get; set; }
|
||||
|
||||
[JsonProperty("japanese")] public string Japanese { get; set; }
|
||||
[JsonProperty("japanese")]
|
||||
public string Japanese { get; set; }
|
||||
|
||||
[JsonProperty("pretty")] public string Pretty { get; set; }
|
||||
[JsonProperty("pretty")]
|
||||
public string Pretty { get; set; }
|
||||
}
|
||||
|
||||
public class Page
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
[JsonProperty("t")]
|
||||
public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
[JsonProperty("w")]
|
||||
public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
[JsonProperty("h")]
|
||||
public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Cover
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
[JsonProperty("t")]
|
||||
public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
[JsonProperty("w")]
|
||||
public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
[JsonProperty("h")]
|
||||
public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnail
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
[JsonProperty("t")]
|
||||
public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
[JsonProperty("w")]
|
||||
public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
[JsonProperty("h")]
|
||||
public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Images
|
||||
{
|
||||
[JsonProperty("pages")] public List<Page> Pages { get; set; }
|
||||
[JsonProperty("pages")]
|
||||
public List<Page> Pages { get; set; }
|
||||
|
||||
[JsonProperty("cover")] public Cover Cover { get; set; }
|
||||
[JsonProperty("cover")]
|
||||
public Cover Cover { get; set; }
|
||||
|
||||
[JsonProperty("thumbnail")] public Thumbnail Thumbnail { get; set; }
|
||||
[JsonProperty("thumbnail")]
|
||||
public Thumbnail Thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public class Tag
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("url")] public string Url { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("count")] public int Count { get; set; }
|
||||
[JsonProperty("count")]
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class Gallery
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("media_id")] public string MediaId { get; set; }
|
||||
[JsonProperty("media_id")]
|
||||
public string MediaId { get; set; }
|
||||
|
||||
[JsonProperty("title")] public Title Title { get; set; }
|
||||
[JsonProperty("title")]
|
||||
public Title Title { get; set; }
|
||||
|
||||
[JsonProperty("images")] public Images Images { get; set; }
|
||||
[JsonProperty("images")]
|
||||
public Images Images { get; set; }
|
||||
|
||||
[JsonProperty("scanlator")] public string Scanlator { get; set; }
|
||||
[JsonProperty("scanlator")]
|
||||
public string Scanlator { get; set; }
|
||||
|
||||
[JsonProperty("upload_date")] public double UploadDate { get; set; }
|
||||
[JsonProperty("upload_date")]
|
||||
public double UploadDate { get; set; }
|
||||
|
||||
[JsonProperty("tags")] public Tag[] Tags { get; set; }
|
||||
[JsonProperty("tags")]
|
||||
public Tag[] Tags { get; set; }
|
||||
|
||||
[JsonProperty("num_pages")] public int NumPages { get; set; }
|
||||
[JsonProperty("num_pages")]
|
||||
public int NumPages { get; set; }
|
||||
|
||||
[JsonProperty("num_favorites")] public int NumFavorites { get; set; }
|
||||
[JsonProperty("num_favorites")]
|
||||
public int NumFavorites { get; set; }
|
||||
}
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
[JsonProperty("result")] public Gallery[] Result { get; set; }
|
||||
[JsonProperty("result")]
|
||||
public Gallery[] Result { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,4 +11,4 @@ public class NovelResult
|
||||
public string Status { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
public string Score { get; set; }
|
||||
}
|
||||
}
|
@@ -10,4 +10,4 @@ public class OmdbMovie
|
||||
public string Genre { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Poster { get; set; }
|
||||
}
|
||||
}
|
@@ -5,45 +5,66 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class OsuUserData
|
||||
{
|
||||
[JsonProperty("user_id")] public string UserId { get; set; }
|
||||
[JsonProperty("user_id")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonProperty("username")] public string Username { get; set; }
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonProperty("join_date")] public string JoinDate { get; set; }
|
||||
[JsonProperty("join_date")]
|
||||
public string JoinDate { get; set; }
|
||||
|
||||
[JsonProperty("count300")] public string Count300 { get; set; }
|
||||
[JsonProperty("count300")]
|
||||
public string Count300 { get; set; }
|
||||
|
||||
[JsonProperty("count100")] public string Count100 { get; set; }
|
||||
[JsonProperty("count100")]
|
||||
public string Count100 { get; set; }
|
||||
|
||||
[JsonProperty("count50")] public string Count50 { get; set; }
|
||||
[JsonProperty("count50")]
|
||||
public string Count50 { get; set; }
|
||||
|
||||
[JsonProperty("playcount")] public string Playcount { get; set; }
|
||||
[JsonProperty("playcount")]
|
||||
public string Playcount { get; set; }
|
||||
|
||||
[JsonProperty("ranked_score")] public string RankedScore { get; set; }
|
||||
[JsonProperty("ranked_score")]
|
||||
public string RankedScore { get; set; }
|
||||
|
||||
[JsonProperty("total_score")] public string TotalScore { get; set; }
|
||||
[JsonProperty("total_score")]
|
||||
public string TotalScore { get; set; }
|
||||
|
||||
[JsonProperty("pp_rank")] public string PpRank { get; set; }
|
||||
[JsonProperty("pp_rank")]
|
||||
public string PpRank { get; set; }
|
||||
|
||||
[JsonProperty("level")] public double Level { get; set; }
|
||||
[JsonProperty("level")]
|
||||
public double Level { get; set; }
|
||||
|
||||
[JsonProperty("pp_raw")] public double PpRaw { get; set; }
|
||||
[JsonProperty("pp_raw")]
|
||||
public double PpRaw { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")] public double Accuracy { get; set; }
|
||||
[JsonProperty("accuracy")]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_ss")] public string CountRankSs { get; set; }
|
||||
[JsonProperty("count_rank_ss")]
|
||||
public string CountRankSs { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_ssh")] public string CountRankSsh { get; set; }
|
||||
[JsonProperty("count_rank_ssh")]
|
||||
public string CountRankSsh { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_s")] public string CountRankS { get; set; }
|
||||
[JsonProperty("count_rank_s")]
|
||||
public string CountRankS { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_sh")] public string CountRankSh { get; set; }
|
||||
[JsonProperty("count_rank_sh")]
|
||||
public string CountRankSh { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_a")] public string CountRankA { get; set; }
|
||||
[JsonProperty("count_rank_a")]
|
||||
public string CountRankA { get; set; }
|
||||
|
||||
[JsonProperty("country")] public string Country { get; set; }
|
||||
[JsonProperty("country")]
|
||||
public string Country { get; set; }
|
||||
|
||||
[JsonProperty("total_seconds_played")] public string TotalSecondsPlayed { get; set; }
|
||||
[JsonProperty("total_seconds_played")]
|
||||
public string TotalSecondsPlayed { get; set; }
|
||||
|
||||
[JsonProperty("pp_country_rank")] public string PpCountryRank { get; set; }
|
||||
}
|
||||
[JsonProperty("pp_country_rank")]
|
||||
public string PpCountryRank { get; set; }
|
||||
}
|
@@ -5,36 +5,36 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class Account
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("league")]
|
||||
public string League { get; set; }
|
||||
[JsonProperty("league")]
|
||||
public string League { get; set; }
|
||||
|
||||
[JsonProperty("classId")]
|
||||
public int ClassId { get; set; }
|
||||
[JsonProperty("classId")]
|
||||
public int ClassId { get; set; }
|
||||
|
||||
[JsonProperty("ascendancyClass")]
|
||||
public int AscendancyClass { get; set; }
|
||||
[JsonProperty("ascendancyClass")]
|
||||
public int AscendancyClass { get; set; }
|
||||
|
||||
[JsonProperty("class")]
|
||||
public string Class { get; set; }
|
||||
[JsonProperty("class")]
|
||||
public string Class { get; set; }
|
||||
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
public class Leagues
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("startAt")]
|
||||
public DateTime StartAt { get; set; }
|
||||
[JsonProperty("startAt")]
|
||||
public DateTime StartAt { get; set; }
|
||||
|
||||
[JsonProperty("endAt")]
|
||||
public object EndAt { get; set; }
|
||||
}
|
||||
[JsonProperty("endAt")]
|
||||
public object EndAt { get; set; }
|
||||
}
|
@@ -7,7 +7,7 @@ public class SteamGameId
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("appid")]
|
||||
public int AppId { get; set; }
|
||||
}
|
||||
@@ -32,4 +32,4 @@ public enum TimeErrors
|
||||
ApiKeyMissing,
|
||||
NotFound,
|
||||
Unknown
|
||||
}
|
||||
}
|
@@ -5,107 +5,153 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class PicartoChannelResponse
|
||||
{
|
||||
[JsonProperty("user_id")] public int UserId { get; set; }
|
||||
[JsonProperty("user_id")]
|
||||
public int UserId { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("avatar")] public string Avatar { get; set; }
|
||||
[JsonProperty("avatar")]
|
||||
public string Avatar { get; set; }
|
||||
|
||||
[JsonProperty("online")] public bool Online { get; set; }
|
||||
[JsonProperty("online")]
|
||||
public bool Online { get; set; }
|
||||
|
||||
[JsonProperty("viewers")] public int Viewers { get; set; }
|
||||
[JsonProperty("viewers")]
|
||||
public int Viewers { get; set; }
|
||||
|
||||
[JsonProperty("viewers_total")] public int ViewersTotal { get; set; }
|
||||
[JsonProperty("viewers_total")]
|
||||
public int ViewersTotal { get; set; }
|
||||
|
||||
[JsonProperty("thumbnails")] public Thumbnails Thumbnails { get; set; }
|
||||
[JsonProperty("thumbnails")]
|
||||
public Thumbnails Thumbnails { get; set; }
|
||||
|
||||
[JsonProperty("followers")] public int Followers { get; set; }
|
||||
[JsonProperty("followers")]
|
||||
public int Followers { get; set; }
|
||||
|
||||
[JsonProperty("subscribers")] public int Subscribers { get; set; }
|
||||
[JsonProperty("subscribers")]
|
||||
public int Subscribers { get; set; }
|
||||
|
||||
[JsonProperty("adult")] public bool Adult { get; set; }
|
||||
[JsonProperty("adult")]
|
||||
public bool Adult { get; set; }
|
||||
|
||||
[JsonProperty("category")] public string Category { get; set; }
|
||||
[JsonProperty("category")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[JsonProperty("account_type")] public string AccountType { get; set; }
|
||||
[JsonProperty("account_type")]
|
||||
public string AccountType { get; set; }
|
||||
|
||||
[JsonProperty("commissions")] public bool Commissions { get; set; }
|
||||
[JsonProperty("commissions")]
|
||||
public bool Commissions { get; set; }
|
||||
|
||||
[JsonProperty("recordings")] public bool Recordings { get; set; }
|
||||
[JsonProperty("recordings")]
|
||||
public bool Recordings { get; set; }
|
||||
|
||||
[JsonProperty("title")] public string Title { get; set; }
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("description_panels")] public List<DescriptionPanel> DescriptionPanels { get; set; }
|
||||
[JsonProperty("description_panels")]
|
||||
public List<DescriptionPanel> DescriptionPanels { get; set; }
|
||||
|
||||
[JsonProperty("private")] public bool Private { get; set; }
|
||||
[JsonProperty("private")]
|
||||
public bool Private { get; set; }
|
||||
|
||||
[JsonProperty("private_message")] public string PrivateMessage { get; set; }
|
||||
[JsonProperty("private_message")]
|
||||
public string PrivateMessage { get; set; }
|
||||
|
||||
[JsonProperty("gaming")] public bool Gaming { get; set; }
|
||||
[JsonProperty("gaming")]
|
||||
public bool Gaming { get; set; }
|
||||
|
||||
[JsonProperty("chat_settings")] public ChatSettings ChatSettings { get; set; }
|
||||
[JsonProperty("chat_settings")]
|
||||
public ChatSettings ChatSettings { get; set; }
|
||||
|
||||
[JsonProperty("last_live")] public DateTime LastLive { get; set; }
|
||||
[JsonProperty("last_live")]
|
||||
public DateTime LastLive { get; set; }
|
||||
|
||||
[JsonProperty("tags")] public List<string> Tags { get; set; }
|
||||
[JsonProperty("tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
[JsonProperty("multistream")] public List<Multistream> Multistream { get; set; }
|
||||
[JsonProperty("multistream")]
|
||||
public List<Multistream> Multistream { get; set; }
|
||||
|
||||
[JsonProperty("languages")] public List<Language> Languages { get; set; }
|
||||
[JsonProperty("languages")]
|
||||
public List<Language> Languages { get; set; }
|
||||
|
||||
[JsonProperty("following")] public bool Following { get; set; }
|
||||
[JsonProperty("following")]
|
||||
public bool Following { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnails
|
||||
{
|
||||
[JsonProperty("web")] public string Web { get; set; }
|
||||
[JsonProperty("web")]
|
||||
public string Web { get; set; }
|
||||
|
||||
[JsonProperty("web_large")] public string WebLarge { get; set; }
|
||||
[JsonProperty("web_large")]
|
||||
public string WebLarge { get; set; }
|
||||
|
||||
[JsonProperty("mobile")] public string Mobile { get; set; }
|
||||
[JsonProperty("mobile")]
|
||||
public string Mobile { get; set; }
|
||||
|
||||
[JsonProperty("tablet")] public string Tablet { get; set; }
|
||||
[JsonProperty("tablet")]
|
||||
public string Tablet { get; set; }
|
||||
}
|
||||
|
||||
public class DescriptionPanel
|
||||
{
|
||||
[JsonProperty("title")] public string Title { get; set; }
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("body")] public string Body { get; set; }
|
||||
[JsonProperty("body")]
|
||||
public string Body { get; set; }
|
||||
|
||||
[JsonProperty("image")] public string Image { get; set; }
|
||||
[JsonProperty("image")]
|
||||
public string Image { get; set; }
|
||||
|
||||
[JsonProperty("image_link")] public string ImageLink { get; set; }
|
||||
[JsonProperty("image_link")]
|
||||
public string ImageLink { get; set; }
|
||||
|
||||
[JsonProperty("button_text")] public string ButtonText { get; set; }
|
||||
[JsonProperty("button_text")]
|
||||
public string ButtonText { get; set; }
|
||||
|
||||
[JsonProperty("button_link")] public string ButtonLink { get; set; }
|
||||
[JsonProperty("button_link")]
|
||||
public string ButtonLink { get; set; }
|
||||
|
||||
[JsonProperty("position")] public int Position { get; set; }
|
||||
[JsonProperty("position")]
|
||||
public int Position { get; set; }
|
||||
}
|
||||
|
||||
public class ChatSettings
|
||||
{
|
||||
[JsonProperty("guest_chat")] public bool GuestChat { get; set; }
|
||||
[JsonProperty("guest_chat")]
|
||||
public bool GuestChat { get; set; }
|
||||
|
||||
[JsonProperty("links")] public bool Links { get; set; }
|
||||
[JsonProperty("links")]
|
||||
public bool Links { get; set; }
|
||||
|
||||
[JsonProperty("level")] public int Level { get; set; }
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
public class Multistream
|
||||
{
|
||||
[JsonProperty("user_id")] public int UserId { get; set; }
|
||||
[JsonProperty("user_id")]
|
||||
public int UserId { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("online")] public bool Online { get; set; }
|
||||
[JsonProperty("online")]
|
||||
public bool Online { get; set; }
|
||||
|
||||
[JsonProperty("adult")] public bool Adult { get; set; }
|
||||
[JsonProperty("adult")]
|
||||
public bool Adult { get; set; }
|
||||
}
|
||||
|
||||
public class Language
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
}
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
@@ -16,5 +16,6 @@ public class StreamData
|
||||
public string StreamUrl { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
|
||||
public StreamDataKey CreateKey() => new(StreamType, UniqueName.ToLower());
|
||||
}
|
||||
public StreamDataKey CreateKey()
|
||||
=> new(StreamType, UniqueName.ToLower());
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
@@ -8,11 +8,11 @@ public readonly struct StreamDataKey
|
||||
{
|
||||
public FollowedStream.FType Type { get; }
|
||||
public string Name { get; }
|
||||
|
||||
|
||||
[JsonConstructor]
|
||||
public StreamDataKey(FollowedStream.FType type, string name)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,4 +19,4 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
// public string ThumbnailUrl { get; set; }
|
||||
// public DateTime StartedAt { get; set; }
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -9,75 +9,106 @@ public class TwitchResponseV5
|
||||
|
||||
public class Channel
|
||||
{
|
||||
[JsonProperty("_id")] public int Id { get; set; }
|
||||
[JsonProperty("_id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("broadcaster_language")] public string BroadcasterLanguage { get; set; }
|
||||
[JsonProperty("broadcaster_language")]
|
||||
public string BroadcasterLanguage { get; set; }
|
||||
|
||||
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("display_name")] public string DisplayName { get; set; }
|
||||
[JsonProperty("display_name")]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[JsonProperty("followers")] public int Followers { get; set; }
|
||||
[JsonProperty("followers")]
|
||||
public int Followers { get; set; }
|
||||
|
||||
[JsonProperty("game")] public string Game { get; set; }
|
||||
[JsonProperty("game")]
|
||||
public string Game { get; set; }
|
||||
|
||||
[JsonProperty("language")] public string Language { get; set; }
|
||||
[JsonProperty("language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
[JsonProperty("logo")] public string Logo { get; set; }
|
||||
[JsonProperty("logo")]
|
||||
public string Logo { get; set; }
|
||||
|
||||
[JsonProperty("mature")] public bool Mature { get; set; }
|
||||
[JsonProperty("mature")]
|
||||
public bool Mature { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("partner")] public bool Partner { get; set; }
|
||||
[JsonProperty("partner")]
|
||||
public bool Partner { get; set; }
|
||||
|
||||
[JsonProperty("profile_banner")] public string ProfileBanner { get; set; }
|
||||
[JsonProperty("profile_banner")]
|
||||
public string ProfileBanner { get; set; }
|
||||
|
||||
[JsonProperty("profile_banner_background_color")]
|
||||
public object ProfileBannerBackgroundColor { get; set; }
|
||||
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("updated_at")] public DateTime UpdatedAt { get; set; }
|
||||
[JsonProperty("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonProperty("url")] public string Url { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("video_banner")] public string VideoBanner { get; set; }
|
||||
[JsonProperty("video_banner")]
|
||||
public string VideoBanner { get; set; }
|
||||
|
||||
[JsonProperty("views")] public int Views { get; set; }
|
||||
[JsonProperty("views")]
|
||||
public int Views { get; set; }
|
||||
}
|
||||
|
||||
public class Preview
|
||||
{
|
||||
[JsonProperty("large")] public string Large { get; set; }
|
||||
[JsonProperty("large")]
|
||||
public string Large { get; set; }
|
||||
|
||||
[JsonProperty("medium")] public string Medium { get; set; }
|
||||
[JsonProperty("medium")]
|
||||
public string Medium { get; set; }
|
||||
|
||||
[JsonProperty("small")] public string Small { get; set; }
|
||||
[JsonProperty("small")]
|
||||
public string Small { get; set; }
|
||||
|
||||
[JsonProperty("template")] public string Template { get; set; }
|
||||
[JsonProperty("template")]
|
||||
public string Template { get; set; }
|
||||
}
|
||||
|
||||
public class Stream
|
||||
{
|
||||
[JsonProperty("_id")] public long Id { get; set; }
|
||||
[JsonProperty("_id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonProperty("average_fps")] public double AverageFps { get; set; }
|
||||
[JsonProperty("average_fps")]
|
||||
public double AverageFps { get; set; }
|
||||
|
||||
[JsonProperty("channel")] public Channel Channel { get; set; }
|
||||
[JsonProperty("channel")]
|
||||
public Channel Channel { get; set; }
|
||||
|
||||
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("delay")] public double Delay { get; set; }
|
||||
[JsonProperty("delay")]
|
||||
public double Delay { get; set; }
|
||||
|
||||
[JsonProperty("game")] public string Game { get; set; }
|
||||
[JsonProperty("game")]
|
||||
public string Game { get; set; }
|
||||
|
||||
[JsonProperty("is_playlist")] public bool IsPlaylist { get; set; }
|
||||
[JsonProperty("is_playlist")]
|
||||
public bool IsPlaylist { get; set; }
|
||||
|
||||
[JsonProperty("preview")] public Preview Preview { get; set; }
|
||||
[JsonProperty("preview")]
|
||||
public Preview Preview { get; set; }
|
||||
|
||||
[JsonProperty("video_height")] public int VideoHeight { get; set; }
|
||||
[JsonProperty("video_height")]
|
||||
public int VideoHeight { get; set; }
|
||||
|
||||
[JsonProperty("viewers")] public int Viewers { get; set; }
|
||||
[JsonProperty("viewers")]
|
||||
public int Viewers { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,13 +5,13 @@ namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class TwitchUsersResponseV5
|
||||
{
|
||||
[JsonProperty("users")] public List<User> Users { get; set; }
|
||||
[JsonProperty("users")]
|
||||
public List<User> Users { get; set; }
|
||||
|
||||
public class User
|
||||
{
|
||||
|
||||
[JsonProperty("_id")]
|
||||
public string Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
|
||||
// [JsonProperty("bio")]
|
||||
// public string Bio { get; set; }
|
||||
@@ -33,6 +33,5 @@ public class TwitchUsersResponseV5
|
||||
//
|
||||
// [JsonProperty("updated_at")]
|
||||
// public DateTime UpdatedAt { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
|
||||
@@ -7,158 +7,144 @@ namespace NadekoBot.Modules.Searches.Common.StreamNotifications;
|
||||
|
||||
public class NotifChecker
|
||||
{
|
||||
private readonly ConnectionMultiplexer _multi;
|
||||
private readonly string _key;
|
||||
|
||||
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
|
||||
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
|
||||
private readonly ConnectionMultiplexer _multi;
|
||||
private readonly string _key;
|
||||
|
||||
private readonly Dictionary<FollowedStream.FType, Provider> _streamProviders;
|
||||
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
|
||||
|
||||
public NotifChecker(IHttpClientFactory httpClientFactory, ConnectionMultiplexer multi, string uniqueCacheKey,
|
||||
public NotifChecker(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ConnectionMultiplexer multi,
|
||||
string uniqueCacheKey,
|
||||
bool isMaster)
|
||||
{
|
||||
_multi = multi;
|
||||
_key = $"{uniqueCacheKey}_followed_streams_data";
|
||||
_streamProviders = new()
|
||||
{
|
||||
{FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory)},
|
||||
{FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory)}
|
||||
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) },
|
||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }
|
||||
};
|
||||
_offlineBuffer = new();
|
||||
if (isMaster)
|
||||
{
|
||||
CacheClearAllData();
|
||||
}
|
||||
if (isMaster) CacheClearAllData();
|
||||
}
|
||||
|
||||
|
||||
// gets all streams which have been failing for more than the provided timespan
|
||||
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
|
||||
{
|
||||
var toReturn = _streamProviders.SelectMany(prov => prov.Value
|
||||
.FailingStreams
|
||||
.Where(fs => DateTime.UtcNow - fs.ErroringSince > duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Item1)))
|
||||
.ToList();
|
||||
.FailingStreams
|
||||
.Where(fs => DateTime.UtcNow - fs.ErroringSince
|
||||
> duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform,
|
||||
fs.Item1)))
|
||||
.ToList();
|
||||
|
||||
if (remove)
|
||||
{
|
||||
foreach (var toBeRemoved in toReturn)
|
||||
{
|
||||
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public Task RunAsync() => Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
public Task RunAsync()
|
||||
=> Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var allStreamData = CacheGetAllData();
|
||||
|
||||
var oldStreamDataDict = allStreamData
|
||||
// group by type
|
||||
.GroupBy(entry => entry.Key.Type)
|
||||
.ToDictionary(
|
||||
entry => entry.Key,
|
||||
entry => entry.AsEnumerable().ToDictionary(x => x.Key.Name, x => x.Value)
|
||||
);
|
||||
|
||||
var newStreamData = await oldStreamDataDict
|
||||
.Select(x =>
|
||||
{
|
||||
// get all stream data for the streams of this type
|
||||
if (_streamProviders.TryGetValue(x.Key, out var provider))
|
||||
{
|
||||
return provider.GetStreamDataAsync(x.Value.Select(entry => entry.Key).ToList());
|
||||
}
|
||||
|
||||
// this means there's no provider for this stream data, (and there was before?)
|
||||
return Task.FromResult(new List<StreamData>());
|
||||
}).WhenAll();
|
||||
|
||||
var newlyOnline = new List<StreamData>();
|
||||
var newlyOffline = new List<StreamData>();
|
||||
// go through all new stream data, compare them with the old ones
|
||||
foreach (var newData in newStreamData.SelectMany(x => x))
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
// update cached data
|
||||
var key = newData.CreateKey();
|
||||
CacheAddData(key, newData, replace: true);
|
||||
var allStreamData = CacheGetAllData();
|
||||
|
||||
// compare old data with new data
|
||||
var oldData = oldStreamDataDict[key.Type][key.Name];
|
||||
var oldStreamDataDict = allStreamData
|
||||
// group by type
|
||||
.GroupBy(entry => entry.Key.Type)
|
||||
.ToDictionary(entry => entry.Key,
|
||||
entry => entry.AsEnumerable()
|
||||
.ToDictionary(x => x.Key.Name, x => x.Value));
|
||||
|
||||
// this is the first pass
|
||||
if (oldData is null)
|
||||
continue;
|
||||
var newStreamData = await oldStreamDataDict.Select(x =>
|
||||
{
|
||||
// get all stream data for the streams of this type
|
||||
if (_streamProviders.TryGetValue(x.Key,
|
||||
out var provider))
|
||||
return provider.GetStreamDataAsync(x.Value
|
||||
.Select(entry => entry.Key)
|
||||
.ToList());
|
||||
|
||||
// if the stream is offline, we need to check if it was
|
||||
// marked as offline once previously
|
||||
// if it was, that means this is second time we're getting offline
|
||||
// status for that stream -> notify subscribers
|
||||
// Note: This is done because twitch api will sometimes return an offline status
|
||||
// shortly after the stream is already online, which causes duplicate notifications.
|
||||
// (stream is online -> stream is offline -> stream is online again (and stays online))
|
||||
// This offlineBuffer will make it so that the stream has to be marked as offline TWICE
|
||||
// before it sends an offline notification to the subscribers.
|
||||
var streamId = (key.Type, key.Name);
|
||||
if (!newData.IsLive && _offlineBuffer.Remove(streamId))
|
||||
// this means there's no provider for this stream data, (and there was before?)
|
||||
return Task.FromResult(new List<StreamData>());
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var newlyOnline = new List<StreamData>();
|
||||
var newlyOffline = new List<StreamData>();
|
||||
// go through all new stream data, compare them with the old ones
|
||||
foreach (var newData in newStreamData.SelectMany(x => x))
|
||||
{
|
||||
newlyOffline.Add(newData);
|
||||
}
|
||||
else if (newData.IsLive != oldData.IsLive)
|
||||
{
|
||||
if (newData.IsLive)
|
||||
// update cached data
|
||||
var key = newData.CreateKey();
|
||||
CacheAddData(key, newData, true);
|
||||
|
||||
// compare old data with new data
|
||||
var oldData = oldStreamDataDict[key.Type][key.Name];
|
||||
|
||||
// this is the first pass
|
||||
if (oldData is null)
|
||||
continue;
|
||||
|
||||
// if the stream is offline, we need to check if it was
|
||||
// marked as offline once previously
|
||||
// if it was, that means this is second time we're getting offline
|
||||
// status for that stream -> notify subscribers
|
||||
// Note: This is done because twitch api will sometimes return an offline status
|
||||
// shortly after the stream is already online, which causes duplicate notifications.
|
||||
// (stream is online -> stream is offline -> stream is online again (and stays online))
|
||||
// This offlineBuffer will make it so that the stream has to be marked as offline TWICE
|
||||
// before it sends an offline notification to the subscribers.
|
||||
var streamId = (key.Type, key.Name);
|
||||
if (!newData.IsLive && _offlineBuffer.Remove(streamId))
|
||||
{
|
||||
_offlineBuffer.Remove(streamId);
|
||||
newlyOnline.Add(newData);
|
||||
newlyOffline.Add(newData);
|
||||
}
|
||||
else
|
||||
else if (newData.IsLive != oldData.IsLive)
|
||||
{
|
||||
_offlineBuffer.Add(streamId);
|
||||
// newlyOffline.Add(newData);
|
||||
if (newData.IsLive)
|
||||
{
|
||||
_offlineBuffer.Remove(streamId);
|
||||
newlyOnline.Add(newData);
|
||||
}
|
||||
else
|
||||
{
|
||||
_offlineBuffer.Add(streamId);
|
||||
// newlyOffline.Add(newData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
Task.Delay(30_000)
|
||||
};
|
||||
|
||||
if (newlyOnline.Count > 0)
|
||||
{
|
||||
tasks.Add(OnStreamsOnline(newlyOnline));
|
||||
}
|
||||
var tasks = new List<Task> { Task.Delay(30_000) };
|
||||
|
||||
if (newlyOffline.Count > 0)
|
||||
{
|
||||
tasks.Add(OnStreamsOffline(newlyOffline));
|
||||
}
|
||||
if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (newlyOffline.Count > 0) tasks.Add(OnStreamsOffline(newlyOffline));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
return db.HashSet(
|
||||
_key,
|
||||
return db.HashSet(_key,
|
||||
JsonConvert.SerializeObject(key),
|
||||
JsonConvert.SerializeObject(data),
|
||||
when: replace ? When.Always : When.NotExists);
|
||||
replace ? When.Always : When.NotExists);
|
||||
}
|
||||
|
||||
public void CacheDeleteData(StreamDataKey key)
|
||||
@@ -176,17 +162,13 @@ public class NotifChecker
|
||||
public Dictionary<StreamDataKey, StreamData?> CacheGetAllData()
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
if (!db.KeyExists(_key))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
if (!db.KeyExists(_key)) return new();
|
||||
|
||||
return db.HashGetAll(_key)
|
||||
.ToDictionary(
|
||||
entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
|
||||
entry => entry.Value.IsNullOrEmpty
|
||||
? default(StreamData)
|
||||
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
|
||||
.ToDictionary(entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
|
||||
entry => entry.Value.IsNullOrEmpty
|
||||
? default
|
||||
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
|
||||
}
|
||||
|
||||
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
@@ -207,7 +189,7 @@ public class NotifChecker
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return currently available stream data, get new one if none available, and start tracking the stream.
|
||||
/// Return currently available stream data, get new one if none available, and start tracking the stream.
|
||||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns>Stream data, if any</returns>
|
||||
@@ -219,7 +201,7 @@ public class NotifChecker
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure a stream is tracked using its stream data.
|
||||
/// Make sure a stream is tracked using its stream data.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to try to track if not already tracked</param>
|
||||
/// <returns>Whether it's newly added</returns>
|
||||
@@ -231,9 +213,9 @@ public class NotifChecker
|
||||
|
||||
// if stream is found, add it to the cache for tracking only if it doesn't already exist
|
||||
// because stream will be checked and events will fire in a loop. We don't want to override old state
|
||||
return CacheAddData(data.CreateKey(), data, replace: false);
|
||||
return CacheAddData(data.CreateKey(), data, false);
|
||||
}
|
||||
|
||||
public void UntrackStreamByKey(in StreamDataKey key)
|
||||
=> CacheDeleteData(key);
|
||||
}
|
||||
}
|
@@ -1,17 +1,18 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class PicartoProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private static Regex Regex { get; } = new(@"picarto.tv/(?<name>.+[^/])/?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override FollowedStream.FType Platform => FollowedStream.FType.Picarto;
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Picarto;
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public PicartoProvider(IHttpClientFactory httpClientFactory)
|
||||
=> _httpClientFactory = httpClientFactory;
|
||||
@@ -40,12 +41,12 @@ public class PicartoProvider : Provider
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string> {id});
|
||||
var data = await GetStreamDataAsync(new List<string> { id });
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async override Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new();
|
||||
@@ -53,17 +54,17 @@ public class PicartoProvider : Provider
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
var toReturn = new List<StreamData>();
|
||||
foreach (var login in logins)
|
||||
{
|
||||
try
|
||||
{
|
||||
http.DefaultRequestHeaders.Accept.Add(new("application/json"));
|
||||
// get id based on the username
|
||||
var res = await http.GetAsync($"https://api.picarto.tv/v1/channel/name/{login}");
|
||||
|
||||
|
||||
if (!res.IsSuccessStatusCode)
|
||||
continue;
|
||||
|
||||
var userData = JsonConvert.DeserializeObject<PicartoChannelResponse>(await res.Content.ReadAsStringAsync())!;
|
||||
|
||||
var userData =
|
||||
JsonConvert.DeserializeObject<PicartoChannelResponse>(await res.Content.ReadAsStringAsync())!;
|
||||
|
||||
toReturn.Add(ToStreamData(userData));
|
||||
_failingStreams.TryRemove(login, out _);
|
||||
@@ -73,11 +74,9 @@ public class PicartoProvider : Provider
|
||||
Log.Warning("Something went wrong retreiving {StreamPlatform} stream data for {Login}: {ErrorMessage}",
|
||||
Platform,
|
||||
login,
|
||||
ex.Message
|
||||
);
|
||||
ex.Message);
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -96,4 +95,4 @@ public class PicartoProvider : Provider
|
||||
StreamUrl = $"https://picarto.tv/{stream.Name}",
|
||||
AvatarUrl = stream.Avatar
|
||||
};
|
||||
}
|
||||
}
|
@@ -3,55 +3,55 @@
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class implemented by providers of all supported platforms
|
||||
/// Abstract class implemented by providers of all supported platforms
|
||||
/// </summary>
|
||||
public abstract class Provider
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the platform.
|
||||
/// Type of the platform.
|
||||
/// </summary>
|
||||
public abstract FollowedStream.FType Platform { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified url is a valid stream url for this platform.
|
||||
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
|
||||
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
|
||||
/// </summary>
|
||||
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams
|
||||
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// When was the first time the stream continually had errors while being retrieved
|
||||
/// </summary>
|
||||
protected readonly ConcurrentDictionary<string, DateTime> _failingStreams = new();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified url is a valid stream url for this platform.
|
||||
/// </summary>
|
||||
/// <param name="url">Url to check</param>
|
||||
/// <returns>True if valid, otherwise false</returns>
|
||||
public abstract Task<bool> IsValidUrl(string url);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the stream on the specified url on this <see cref="Platform"/>
|
||||
/// Gets stream data of the stream on the specified url on this <see cref="Platform" />
|
||||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns><see cref="StreamData"/> of the specified stream. Null if none found</returns>
|
||||
/// <returns><see cref="StreamData" /> of the specified stream. Null if none found</returns>
|
||||
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the specified id/username on this <see cref="Platform"/>
|
||||
/// Gets stream data of the specified id/username on this <see cref="Platform" />
|
||||
/// </summary>
|
||||
/// <param name="id">Name (or id where applicable) of the user on the platform</param>
|
||||
/// <returns><see cref="StreamData"/> of the user. Null if none found</returns>
|
||||
/// <returns><see cref="StreamData" /> of the user. Null if none found</returns>
|
||||
public abstract Task<StreamData?> GetStreamDataAsync(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of all specified ids/usernames on this <see cref="Platform"/>
|
||||
/// Gets stream data of all specified ids/usernames on this <see cref="Platform" />
|
||||
/// </summary>
|
||||
/// <param name="usernames">List of ids/usernames</param>
|
||||
/// <returns><see cref="StreamData"/> of all users, in the same order. Null for every id/user not found.</returns>
|
||||
/// <returns><see cref="StreamData" /> of all users, in the same order. Null for every id/user not found.</returns>
|
||||
public abstract Task<List<StreamData>> GetStreamDataAsync(List<string> usernames);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
|
||||
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
|
||||
/// </summary>
|
||||
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams =>
|
||||
_failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// When was the first time the stream continually had errors while being retrieved
|
||||
/// </summary>
|
||||
protected readonly ConcurrentDictionary<string, DateTime> _failingStreams = new();
|
||||
|
||||
public void ClearErrorsFor(string login)
|
||||
public void ClearErrorsFor(string login)
|
||||
=> _failingStreams.TryRemove(login, out _);
|
||||
}
|
||||
}
|
@@ -178,4 +178,4 @@
|
||||
// public DateTime StartedAt { get; set; }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -1,17 +1,18 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TwitchProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private static Regex Regex { get; } = new(@"twitch.tv/(?<name>.+[^/])/?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Twitch;
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public TwitchProvider(IHttpClientFactory httpClientFactory)
|
||||
=> _httpClientFactory = httpClientFactory;
|
||||
@@ -40,7 +41,7 @@ public class TwitchProvider : Provider
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string> {id});
|
||||
var data = await GetStreamDataAsync(new List<string> { id });
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
@@ -56,7 +57,6 @@ public class TwitchProvider : Provider
|
||||
|
||||
var toReturn = new List<StreamData>();
|
||||
foreach (var login in logins)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get id based on the username
|
||||
@@ -70,8 +70,7 @@ public class TwitchProvider : Provider
|
||||
|
||||
// get stream data
|
||||
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}");
|
||||
var resObj =
|
||||
JsonConvert.DeserializeAnonymousType(str, new {Stream = new TwitchResponseV5.Stream()});
|
||||
var resObj = JsonConvert.DeserializeAnonymousType(str, new { Stream = new TwitchResponseV5.Stream() });
|
||||
|
||||
// if stream is null, user is not streaming
|
||||
if (resObj?.Stream is null)
|
||||
@@ -79,7 +78,7 @@ public class TwitchProvider : Provider
|
||||
// if user is not streaming, get his offline banner
|
||||
var chStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/channels/{user.Id}");
|
||||
var ch = JsonConvert.DeserializeObject<TwitchResponseV5.Channel>(chStr)!;
|
||||
|
||||
|
||||
toReturn.Add(new()
|
||||
{
|
||||
StreamType = FollowedStream.FType.Twitch,
|
||||
@@ -102,11 +101,9 @@ public class TwitchProvider : Provider
|
||||
Log.Warning("Something went wrong retreiving {StreamPlatform} stream data for {Login}: {ErrorMessage}",
|
||||
Platform,
|
||||
login,
|
||||
ex.Message
|
||||
);
|
||||
ex.Message);
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -125,4 +122,4 @@ public class TwitchProvider : Provider
|
||||
StreamUrl = $"https://twitch.tv/{stream.Channel.Name}",
|
||||
AvatarUrl = stream.Channel.Logo
|
||||
};
|
||||
}
|
||||
}
|
@@ -6,4 +6,4 @@ public class TimeData
|
||||
public string Address { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
public string TimeZoneName { get; set; }
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ public class TimeZoneResult
|
||||
{
|
||||
[JsonProperty("abbreviation")]
|
||||
public string TimezoneName { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")]
|
||||
public int Timestamp { get; set; }
|
||||
}
|
||||
@@ -18,4 +19,4 @@ public class LocationIqResponse
|
||||
|
||||
[JsonProperty("display_name")]
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
}
|
@@ -5,9 +5,10 @@ public class UrbanResponse
|
||||
{
|
||||
public UrbanDef[] List { get; set; }
|
||||
}
|
||||
|
||||
public class UrbanDef
|
||||
{
|
||||
public string Word { get; set; }
|
||||
public string Definition { get; set; }
|
||||
public string Permalink { get; set; }
|
||||
}
|
||||
}
|
@@ -22,8 +22,10 @@ public class Main
|
||||
public double Temp { get; set; }
|
||||
public float Pressure { get; set; }
|
||||
public float Humidity { get; set; }
|
||||
|
||||
[JsonProperty("temp_min")]
|
||||
public double TempMin { get; set; }
|
||||
|
||||
[JsonProperty("temp_max")]
|
||||
public double TempMax { get; set; }
|
||||
}
|
||||
@@ -62,4 +64,4 @@ public class WeatherData
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Cod { get; set; }
|
||||
}
|
||||
}
|
@@ -15,4 +15,4 @@ public class WikipediaApiModel
|
||||
public string FullUrl { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,5 +5,7 @@ public class WoWJoke
|
||||
{
|
||||
public string Question { get; set; }
|
||||
public string Answer { get; set; }
|
||||
public override string ToString() => $"`{Question}`\n\n**{Answer}**";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"`{Question}`\n\n**{Answer}**";
|
||||
}
|
@@ -7,7 +7,8 @@ public partial class Searches
|
||||
{
|
||||
public class CryptoCommands : NadekoSubmodule<CryptoService>
|
||||
{
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Crypto(string name)
|
||||
{
|
||||
name = name?.ToUpperInvariant();
|
||||
@@ -20,13 +21,11 @@ public partial class Searches
|
||||
if (nearest is not null)
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(
|
||||
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
|
||||
if (await PromptUserConfirmAsync(embed))
|
||||
{
|
||||
crypto = nearest;
|
||||
}
|
||||
if (await PromptUserConfirmAsync(embed)) crypto = nearest;
|
||||
}
|
||||
|
||||
if (crypto is null)
|
||||
@@ -44,15 +43,21 @@ public partial class Searches
|
||||
: crypto.Quote.Usd.Percent_Change_24h;
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap), $"${crypto.Quote.Usd.Market_Cap:n0}", true)
|
||||
.AddField(GetText(strs.price), $"${crypto.Quote.Usd.Price}", true)
|
||||
.AddField(GetText(strs.volume_24h), $"${crypto.Quote.Usd.Volume_24h:n0}", true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay}% / {lastDay}%", true)
|
||||
.WithImageUrl($"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{crypto.Id}.png"));
|
||||
.WithOkColor()
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl(
|
||||
$"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap),
|
||||
$"${crypto.Quote.Usd.Market_Cap:n0}",
|
||||
true)
|
||||
.AddField(GetText(strs.price), $"${crypto.Quote.Usd.Price}", true)
|
||||
.AddField(GetText(strs.volume_24h),
|
||||
$"${crypto.Quote.Usd.Volume_24h:n0}",
|
||||
true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay}% / {lastDay}%", true)
|
||||
.WithImageUrl(
|
||||
$"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{crypto.Id}.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
#nullable disable
|
||||
using CodeHollow.FeedReader;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -11,36 +12,35 @@ public partial class Searches
|
||||
{
|
||||
private static readonly Regex YtChannelRegex =
|
||||
new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?<channelid>[a-zA-Z0-9\-]{1,})");
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public Task YtUploadNotif(string url, [Leftover] ITextChannel channel = null)
|
||||
{
|
||||
var m = YtChannelRegex.Match(url);
|
||||
if (!m.Success)
|
||||
{
|
||||
return ReplyErrorLocalizedAsync(strs.invalid_input);
|
||||
}
|
||||
if (!m.Success) return ReplyErrorLocalizedAsync(strs.invalid_input);
|
||||
|
||||
var channelId = m.Groups["channelid"].Value;
|
||||
|
||||
return Feed("https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId, channel);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task Feed(string url, [Leftover] ITextChannel channel = null)
|
||||
{
|
||||
var success = Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||
var success = Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||
if (success)
|
||||
{
|
||||
channel ??= (ITextChannel)ctx.Channel;
|
||||
try
|
||||
{
|
||||
var feeds = await CodeHollow.FeedReader.FeedReader.ReadAsync(url);
|
||||
var feeds = await FeedReader.ReadAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -62,20 +62,20 @@ public partial class Searches
|
||||
await ReplyConfirmLocalizedAsync(strs.feed_not_valid);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task FeedRemove(int index)
|
||||
{
|
||||
if (_service.RemoveFeed(ctx.Guild.Id, --index))
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.feed_removed);
|
||||
}
|
||||
else
|
||||
await ReplyErrorLocalizedAsync(strs.feed_out_of_range);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task FeedList()
|
||||
@@ -84,24 +84,22 @@ public partial class Searches
|
||||
|
||||
if (!feeds.Any())
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.feed_no_feed)));
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor().WithDescription(GetText(strs.feed_no_feed)));
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(0, cur =>
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor();
|
||||
var i = 0;
|
||||
var fs = string.Join("\n", feeds.Skip(cur * 10)
|
||||
.Take(10)
|
||||
.Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}"));
|
||||
await ctx.SendPaginatedConfirmAsync(0,
|
||||
cur =>
|
||||
{
|
||||
var embed = _eb.Create().WithOkColor();
|
||||
var i = 0;
|
||||
var fs = string.Join("\n",
|
||||
feeds.Skip(cur * 10).Take(10).Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}"));
|
||||
|
||||
return embed.WithDescription(fs);
|
||||
|
||||
}, feeds.Count, 10);
|
||||
return embed.WithDescription(fs);
|
||||
},
|
||||
feeds.Count,
|
||||
10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,23 +8,26 @@ public partial class Searches
|
||||
[Group]
|
||||
public class JokeCommands : NadekoSubmodule<SearchesService>
|
||||
{
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Yomama()
|
||||
=> await SendConfirmAsync(await _service.GetYomamaJoke());
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Randjoke()
|
||||
{
|
||||
var (setup, punchline) = await _service.GetRandomJoke();
|
||||
await SendConfirmAsync(setup, punchline);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task ChuckNorris()
|
||||
=> await SendConfirmAsync(await _service.GetChuckNorrisJoke());
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task WowJoke()
|
||||
{
|
||||
if (!_service.WowJokes.Any())
|
||||
@@ -32,11 +35,13 @@ public partial class Searches
|
||||
await ReplyErrorLocalizedAsync(strs.jokes_not_loaded);
|
||||
return;
|
||||
}
|
||||
|
||||
var joke = _service.WowJokes[new NadekoRandom().Next(0, _service.WowJokes.Count)];
|
||||
await SendConfirmAsync(joke.Question, joke.Answer);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task MagicItem()
|
||||
{
|
||||
if (!_service.WowJokes.Any())
|
||||
@@ -44,9 +49,10 @@ public partial class Searches
|
||||
await ReplyErrorLocalizedAsync(strs.magicitems_not_loaded);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = _service.MagicItems[new NadekoRandom().Next(0, _service.MagicItems.Count)];
|
||||
|
||||
await SendConfirmAsync("✨" + item.Name, item.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
@@ -10,29 +10,25 @@ public partial class Searches
|
||||
[Group]
|
||||
public class MemegenCommands : NadekoSubmodule
|
||||
{
|
||||
private class MemegenTemplate
|
||||
private static readonly ImmutableDictionary<char, string> _map = new Dictionary<char, string>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
private static readonly ImmutableDictionary<char, string> _map = new Dictionary<char, string>()
|
||||
{
|
||||
{'?', "~q"},
|
||||
{'%', "~p"},
|
||||
{'#', "~h"},
|
||||
{'/', "~s"},
|
||||
{' ', "-"},
|
||||
{'-', "--"},
|
||||
{'_', "__"},
|
||||
{'"', "''"}
|
||||
|
||||
{ '?', "~q" },
|
||||
{ '%', "~p" },
|
||||
{ '#', "~h" },
|
||||
{ '/', "~s" },
|
||||
{ ' ', "-" },
|
||||
{ '-', "--" },
|
||||
{ '_', "__" },
|
||||
{ '"', "''" }
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public MemegenCommands(IHttpClientFactory factory)
|
||||
=> _httpFactory = factory;
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Memelist(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
@@ -42,37 +38,38 @@ public partial class Searches
|
||||
var res = await http.GetAsync("https://api.memegen.link/templates/");
|
||||
|
||||
var rawJson = await res.Content.ReadAsStringAsync();
|
||||
|
||||
|
||||
var data = JsonConvert.DeserializeObject<List<MemegenTemplate>>(rawJson);
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page, curPage =>
|
||||
{
|
||||
var templates = string.Empty;
|
||||
foreach (var template in data.Skip(curPage * 15).Take(15))
|
||||
await ctx.SendPaginatedConfirmAsync(page,
|
||||
curPage =>
|
||||
{
|
||||
templates += $"**{template.Name}:**\n key: `{template.Id}`\n";
|
||||
}
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithDescription(templates);
|
||||
var templates = string.Empty;
|
||||
foreach (var template in data.Skip(curPage * 15).Take(15))
|
||||
templates += $"**{template.Name}:**\n key: `{template.Id}`\n";
|
||||
var embed = _eb.Create().WithOkColor().WithDescription(templates);
|
||||
|
||||
return embed;
|
||||
}, data.Count, 15);
|
||||
return embed;
|
||||
},
|
||||
data.Count,
|
||||
15);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Memegen(string meme, [Leftover] string memeText = null)
|
||||
{
|
||||
var memeUrl = $"http://api.memegen.link/{meme}";
|
||||
if (!string.IsNullOrWhiteSpace(memeText))
|
||||
{
|
||||
var memeTextArray = memeText.Split(';');
|
||||
foreach(var text in memeTextArray)
|
||||
foreach (var text in memeTextArray)
|
||||
{
|
||||
var newText = Replace(text);
|
||||
memeUrl += $"/{newText}";
|
||||
}
|
||||
}
|
||||
|
||||
memeUrl += ".png";
|
||||
await ctx.Channel.SendMessageAsync(memeUrl);
|
||||
}
|
||||
@@ -82,14 +79,18 @@ public partial class Searches
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (_map.TryGetValue(c, out var tmp))
|
||||
sb.Append(tmp);
|
||||
else
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private class MemegenTemplate
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,16 +18,15 @@ public partial class Searches
|
||||
_httpFactory = factory;
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Osu(string user, [Leftover] string mode = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(user))
|
||||
return;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var modeNumber = string.IsNullOrWhiteSpace(mode)
|
||||
? 0
|
||||
: ResolveGameMode(mode);
|
||||
var modeNumber = string.IsNullOrWhiteSpace(mode) ? 0 : ResolveGameMode(mode);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -52,17 +51,18 @@ public partial class Searches
|
||||
var userId = obj.UserId;
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"osu! {smode} profile for {user}")
|
||||
.WithThumbnailUrl($"https://a.ppy.sh/{userId}")
|
||||
.WithDescription($"https://osu.ppy.sh/u/{userId}")
|
||||
.AddField("Official Rank", $"#{obj.PpRank}", true)
|
||||
.AddField("Country Rank", $"#{obj.PpCountryRank} :flag_{obj.Country.ToLower()}:", true)
|
||||
.AddField("Total PP", Math.Round(obj.PpRaw, 2), true)
|
||||
.AddField("Accuracy", Math.Round(obj.Accuracy, 2) + "%", true)
|
||||
.AddField("Playcount", obj.Playcount, true)
|
||||
.AddField("Level", Math.Round(obj.Level), true)
|
||||
);
|
||||
.WithOkColor()
|
||||
.WithTitle($"osu! {smode} profile for {user}")
|
||||
.WithThumbnailUrl($"https://a.ppy.sh/{userId}")
|
||||
.WithDescription($"https://osu.ppy.sh/u/{userId}")
|
||||
.AddField("Official Rank", $"#{obj.PpRank}", true)
|
||||
.AddField("Country Rank",
|
||||
$"#{obj.PpCountryRank} :flag_{obj.Country.ToLower()}:",
|
||||
true)
|
||||
.AddField("Total PP", Math.Round(obj.PpRaw, 2), true)
|
||||
.AddField("Accuracy", Math.Round(obj.Accuracy, 2) + "%", true)
|
||||
.AddField("Playcount", obj.Playcount, true)
|
||||
.AddField("Level", Math.Round(obj.Level), true));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
@@ -75,17 +75,15 @@ public partial class Searches
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Gatari(string user, [Leftover] string mode = null)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var modeNumber = string.IsNullOrWhiteSpace(mode)
|
||||
? 0
|
||||
: ResolveGameMode(mode);
|
||||
var modeNumber = string.IsNullOrWhiteSpace(mode) ? 0 : ResolveGameMode(mode);
|
||||
|
||||
var modeStr = ResolveGameMode(modeNumber);
|
||||
var resString = await http
|
||||
.GetStringAsync($"https://api.gatari.pw/user/stats?u={user}&mode={modeNumber}");
|
||||
var resString = await http.GetStringAsync($"https://api.gatari.pw/user/stats?u={user}&mode={modeNumber}");
|
||||
|
||||
var statsResponse = JsonConvert.DeserializeObject<GatariUserStatsResponse>(resString);
|
||||
if (statsResponse.Code != 200 || statsResponse.Stats.Id == 0)
|
||||
@@ -100,23 +98,27 @@ public partial class Searches
|
||||
var userStats = statsResponse.Stats;
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"osu!Gatari {modeStr} profile for {user}")
|
||||
.WithThumbnailUrl($"https://a.gatari.pw/{userStats.Id}")
|
||||
.WithDescription($"https://osu.gatari.pw/u/{userStats.Id}")
|
||||
.AddField("Official Rank", $"#{userStats.Rank}", true)
|
||||
.AddField("Country Rank", $"#{userStats.CountryRank} :flag_{userData.Country.ToLower()}:", true)
|
||||
.AddField("Total PP", userStats.Pp, true)
|
||||
.AddField("Accuracy", $"{Math.Round(userStats.AvgAccuracy, 2)}%", true)
|
||||
.AddField("Playcount", userStats.Playcount, true)
|
||||
.AddField("Level", userStats.Level, true);
|
||||
.WithOkColor()
|
||||
.WithTitle($"osu!Gatari {modeStr} profile for {user}")
|
||||
.WithThumbnailUrl($"https://a.gatari.pw/{userStats.Id}")
|
||||
.WithDescription($"https://osu.gatari.pw/u/{userStats.Id}")
|
||||
.AddField("Official Rank", $"#{userStats.Rank}", true)
|
||||
.AddField("Country Rank",
|
||||
$"#{userStats.CountryRank} :flag_{userData.Country.ToLower()}:",
|
||||
true)
|
||||
.AddField("Total PP", userStats.Pp, true)
|
||||
.AddField("Accuracy", $"{Math.Round(userStats.AvgAccuracy, 2)}%", true)
|
||||
.AddField("Playcount", userStats.Playcount, true)
|
||||
.AddField("Level", userStats.Level, true);
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Osu5(string user, [Leftover] string mode = null)
|
||||
{;
|
||||
{
|
||||
;
|
||||
if (string.IsNullOrWhiteSpace(_creds.OsuApiKey))
|
||||
{
|
||||
await SendErrorAsync("An osu! API key is required.");
|
||||
@@ -131,26 +133,23 @@ public partial class Searches
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var m = 0;
|
||||
if (!string.IsNullOrWhiteSpace(mode))
|
||||
{
|
||||
m = ResolveGameMode(mode);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(mode)) m = ResolveGameMode(mode);
|
||||
|
||||
var reqString = $"https://osu.ppy.sh/api/get_user_best" +
|
||||
$"?k={_creds.OsuApiKey}" +
|
||||
$"&u={Uri.EscapeDataString(user)}" +
|
||||
$"&type=string" +
|
||||
$"&limit=5" +
|
||||
$"&m={m}";
|
||||
var reqString = "https://osu.ppy.sh/api/get_user_best"
|
||||
+ $"?k={_creds.OsuApiKey}"
|
||||
+ $"&u={Uri.EscapeDataString(user)}"
|
||||
+ "&type=string"
|
||||
+ "&limit=5"
|
||||
+ $"&m={m}";
|
||||
|
||||
var resString = await http.GetStringAsync(reqString);
|
||||
var obj = JsonConvert.DeserializeObject<List<OsuUserBests>>(resString);
|
||||
|
||||
var mapTasks = obj.Select(async item =>
|
||||
{
|
||||
var mapReqString = $"https://osu.ppy.sh/api/get_beatmaps" +
|
||||
$"?k={_creds.OsuApiKey}" +
|
||||
$"&b={item.BeatmapId}";
|
||||
var mapReqString = "https://osu.ppy.sh/api/get_beatmaps"
|
||||
+ $"?k={_creds.OsuApiKey}"
|
||||
+ $"&b={item.BeatmapId}";
|
||||
|
||||
var mapResString = await http.GetStringAsync(mapReqString);
|
||||
var map = JsonConvert.DeserializeObject<List<OsuMapData>>(mapResString).FirstOrDefault();
|
||||
@@ -164,23 +163,15 @@ public partial class Searches
|
||||
var desc = $@"[/b/{item.BeatmapId}](https://osu.ppy.sh/b/{item.BeatmapId})
|
||||
{pp + "pp",-7} | {acc + "%",-7}
|
||||
";
|
||||
if (mods != "+")
|
||||
{
|
||||
desc += Format.Bold(mods);
|
||||
}
|
||||
if (mods != "+") desc += Format.Bold(mods);
|
||||
|
||||
return (title, desc);
|
||||
});
|
||||
|
||||
var eb = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"Top 5 plays for {user}");
|
||||
|
||||
|
||||
var eb = _eb.Create().WithOkColor().WithTitle($"Top 5 plays for {user}");
|
||||
|
||||
var mapData = await mapTasks.WhenAll();
|
||||
foreach (var (title, desc) in mapData.Where(x => x != default))
|
||||
{
|
||||
eb.AddField(title, desc, false);
|
||||
}
|
||||
foreach (var (title, desc) in mapData.Where(x => x != default)) eb.AddField(title, desc);
|
||||
|
||||
await ctx.Channel.EmbedAsync(eb);
|
||||
}
|
||||
@@ -192,11 +183,8 @@ public partial class Searches
|
||||
double totalHits;
|
||||
if (mode == 0)
|
||||
{
|
||||
hitPoints = (play.Count50 * 50) +
|
||||
(play.Count100 * 100) +
|
||||
(play.Count300 * 300);
|
||||
totalHits = play.Count50 + play.Count100 +
|
||||
play.Count300 + play.Countmiss;
|
||||
hitPoints = (play.Count50 * 50) + (play.Count100 * 100) + (play.Count300 * 300);
|
||||
totalHits = play.Count50 + play.Count100 + play.Count300 + play.Countmiss;
|
||||
totalHits *= 300;
|
||||
}
|
||||
else if (mode == 1)
|
||||
@@ -208,18 +196,22 @@ public partial class Searches
|
||||
else if (mode == 2)
|
||||
{
|
||||
hitPoints = play.Count50 + play.Count100 + play.Count300;
|
||||
totalHits = play.Countmiss + play.Count50 + play.Count100 + play.Count300 +
|
||||
play.Countkatu;
|
||||
totalHits = play.Countmiss + play.Count50 + play.Count100 + play.Count300 + play.Countkatu;
|
||||
}
|
||||
else
|
||||
{
|
||||
hitPoints = (play.Count50 * 50) +
|
||||
(play.Count100 * 100) +
|
||||
(play.Countkatu * 200) +
|
||||
((play.Count300 + play.Countgeki) * 300);
|
||||
hitPoints = (play.Count50 * 50)
|
||||
+ (play.Count100 * 100)
|
||||
+ (play.Countkatu * 200)
|
||||
+ ((play.Count300 + play.Countgeki) * 300);
|
||||
|
||||
totalHits = (play.Countmiss + play.Count50 + play.Count100 +
|
||||
play.Countkatu + play.Count300 + play.Countgeki) * 300;
|
||||
totalHits = (play.Countmiss
|
||||
+ play.Count50
|
||||
+ play.Count100
|
||||
+ play.Countkatu
|
||||
+ play.Count300
|
||||
+ play.Countgeki)
|
||||
* 300;
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +258,7 @@ public partial class Searches
|
||||
//https://github.com/ppy/osu-api/wiki#mods
|
||||
private static string ResolveMods(int mods)
|
||||
{
|
||||
var modString = $"+";
|
||||
var modString = "+";
|
||||
|
||||
if (IsBitSet(mods, 0))
|
||||
modString += "NF";
|
||||
@@ -300,7 +292,7 @@ public partial class Searches
|
||||
return modString;
|
||||
}
|
||||
|
||||
private static bool IsBitSet(int mods, int pos) =>
|
||||
(mods & (1 << pos)) != 0;
|
||||
private static bool IsBitSet(int mods, int pos)
|
||||
=> (mods & (1 << pos)) != 0;
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,6 +3,8 @@ using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
@@ -15,16 +17,105 @@ public partial class Searches
|
||||
private const string _poeURL = "https://www.pathofexile.com/character-window/get-characters?accountName=";
|
||||
private const string _ponURL = "http://poe.ninja/api/Data/GetCurrencyOverview?league=";
|
||||
private const string _pogsURL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search=";
|
||||
private const string _pogURL = "https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject=";
|
||||
private const string _pogiURL = "https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:";
|
||||
|
||||
private const string _pogURL =
|
||||
"https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject=";
|
||||
|
||||
private const string _pogiURL =
|
||||
"https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:";
|
||||
|
||||
private const string _profileURL = "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;
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task PathOfExile(string usr, string league = "", int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
@@ -46,53 +137,48 @@ public partial class Searches
|
||||
}
|
||||
catch
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithDescription(GetText(strs.account_not_found))
|
||||
.WithErrorColor();
|
||||
var embed = _eb.Create().WithDescription(GetText(strs.account_not_found)).WithErrorColor();
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(league))
|
||||
{
|
||||
characters.RemoveAll(c => c.League != league);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(league)) characters.RemoveAll(c => c.League != league);
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page, curPage =>
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithAuthor($"Characters on {usr}'s account",
|
||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
||||
$"{_profileURL}{usr}")
|
||||
.WithOkColor();
|
||||
|
||||
var tempList = characters.Skip(curPage * 9).Take(9).ToList();
|
||||
|
||||
if (characters.Count == 0)
|
||||
await ctx.SendPaginatedConfirmAsync(page,
|
||||
curPage =>
|
||||
{
|
||||
return embed.WithDescription("This account has no characters.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var embed = _eb.Create()
|
||||
.WithAuthor($"Characters on {usr}'s account",
|
||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
||||
$"{_profileURL}{usr}")
|
||||
.WithOkColor();
|
||||
|
||||
var tempList = characters.Skip(curPage * 9).Take(9).ToList();
|
||||
|
||||
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 < tempList.Count; i++)
|
||||
{
|
||||
var character = tempList[i];
|
||||
|
||||
sb.AppendLine($"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}");
|
||||
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;
|
||||
}
|
||||
}, characters.Count, 9, true);
|
||||
},
|
||||
characters.Count,
|
||||
9);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task PathOfExileLeagues()
|
||||
{
|
||||
var leagues = new List<Leagues>();
|
||||
@@ -105,21 +191,19 @@ public partial class Searches
|
||||
}
|
||||
catch
|
||||
{
|
||||
var eembed = _eb.Create()
|
||||
.WithDescription(GetText(strs.leagues_not_found))
|
||||
.WithErrorColor();
|
||||
var eembed = _eb.Create().WithDescription(GetText(strs.leagues_not_found)).WithErrorColor();
|
||||
|
||||
await ctx.Channel.EmbedAsync(eembed);
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithAuthor($"Path of Exile Leagues",
|
||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
||||
"https://www.pathofexile.com")
|
||||
.WithOkColor();
|
||||
.WithAuthor("Path of Exile Leagues",
|
||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
||||
"https://www.pathofexile.com")
|
||||
.WithOkColor();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"```{"#",-5}{"League Name",-23}");
|
||||
for (var i = 0; i < leagues.Count; i++)
|
||||
{
|
||||
@@ -127,6 +211,7 @@ public partial class Searches
|
||||
|
||||
sb.AppendLine($"#{i + 1,-4}{league.Id,-23}");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
@@ -134,7 +219,8 @@ public partial class Searches
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task PathOfExileCurrency(string leagueName, string currencyName, string convertName = "Chaos Orb")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(leagueName))
|
||||
@@ -142,6 +228,7 @@ public partial class Searches
|
||||
await SendErrorAsync("Please provide league name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currencyName))
|
||||
{
|
||||
await SendErrorAsync("Please provide currency name.");
|
||||
@@ -167,10 +254,12 @@ public partial class Searches
|
||||
}
|
||||
else
|
||||
{
|
||||
var currencyInput = obj["lines"].Values<JObject>()
|
||||
.Where(i => i["currencyTypeName"].Value<string>() == cleanCurrency)
|
||||
.FirstOrDefault();
|
||||
chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(), System.Globalization.CultureInfo.InvariantCulture);
|
||||
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")
|
||||
@@ -179,121 +268,35 @@ public partial class Searches
|
||||
}
|
||||
else
|
||||
{
|
||||
var currencyOutput = obj["lines"].Values<JObject>()
|
||||
.Where(i => i["currencyTypeName"].Value<string>() == cleanConvert)
|
||||
.FirstOrDefault();
|
||||
conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(), System.Globalization.CultureInfo.InvariantCulture);
|
||||
var currencyOutput = obj["lines"]
|
||||
.Values<JObject>()
|
||||
.Where(i => i["currencyTypeName"].Value<string>() == cleanConvert)
|
||||
.FirstOrDefault();
|
||||
conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(),
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var embed = _eb.Create()
|
||||
.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();
|
||||
.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 ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithDescription(GetText(strs.ninja_not_found))
|
||||
.WithErrorColor();
|
||||
var embed = _eb.Create().WithDescription(GetText(strs.ninja_not_found)).WithErrorColor();
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
}
|
||||
|
||||
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" }
|
||||
};
|
||||
|
||||
private string ShortCurrencyName(string str)
|
||||
{
|
||||
if (currencyDictionary.ContainsValue(str))
|
||||
{
|
||||
return str;
|
||||
}
|
||||
if (currencyDictionary.ContainsValue(str)) return str;
|
||||
|
||||
var currency = currencyDictionary[str];
|
||||
|
||||
@@ -307,4 +310,4 @@ public partial class Searches
|
||||
return league;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,9 +6,6 @@ public partial class Searches
|
||||
[Group]
|
||||
public class PlaceCommands : NadekoSubmodule
|
||||
{
|
||||
private static readonly string _typesStr =
|
||||
string.Join(", ", Enum.GetNames(typeof(PlaceType)));
|
||||
|
||||
public enum PlaceType
|
||||
{
|
||||
Cage, //http://www.placecage.com
|
||||
@@ -18,15 +15,18 @@ public partial class Searches
|
||||
Bear, //https://www.placebear.com
|
||||
Kitten, //http://placekitten.com
|
||||
Bacon, //http://baconmockup.com
|
||||
Xoart, //http://xoart.link
|
||||
Xoart //http://xoart.link
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public async Task Placelist()
|
||||
=> await SendConfirmAsync(GetText(strs.list_of_place_tags(Prefix)),
|
||||
_typesStr);
|
||||
private static readonly string _typesStr = string.Join(", ", Enum.GetNames(typeof(PlaceType)));
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Placelist()
|
||||
=> await SendConfirmAsync(GetText(strs.list_of_place_tags(Prefix)), _typesStr);
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Place(PlaceType placeType, uint width = 0, uint height = 0)
|
||||
{
|
||||
var url = string.Empty;
|
||||
@@ -57,6 +57,7 @@ public partial class Searches
|
||||
url = "http://xoart.link";
|
||||
break;
|
||||
}
|
||||
|
||||
var rng = new NadekoRandom();
|
||||
if (width is <= 0 or > 1000)
|
||||
width = (uint)rng.Next(250, 850);
|
||||
@@ -69,4 +70,4 @@ public partial class Searches
|
||||
await ctx.Channel.SendMessageAsync(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using NadekoBot.Common.Pokemon;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
@@ -9,15 +9,19 @@ public partial class Searches
|
||||
[Group]
|
||||
public class PokemonSearchCommands : NadekoSubmodule<SearchesService>
|
||||
{
|
||||
private readonly IDataCache _cache;
|
||||
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
|
||||
=> _cache.LocalData.Pokemons;
|
||||
|
||||
public IReadOnlyDictionary<string, SearchPokemon> Pokemons => _cache.LocalData.Pokemons;
|
||||
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities => _cache.LocalData.PokemonAbilities;
|
||||
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
|
||||
=> _cache.LocalData.PokemonAbilities;
|
||||
|
||||
private readonly IDataCache _cache;
|
||||
|
||||
public PokemonSearchCommands(IDataCache cache)
|
||||
=> _cache = cache;
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Pokemon([Leftover] string pokemon = null)
|
||||
{
|
||||
pokemon = pokemon?.Trim().ToUpperInvariant();
|
||||
@@ -25,43 +29,51 @@ public partial class Searches
|
||||
return;
|
||||
|
||||
foreach (var kvp in Pokemons)
|
||||
{
|
||||
if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant())
|
||||
{
|
||||
var p = kvp.Value;
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithTitle(kvp.Key.ToTitleCase())
|
||||
.WithDescription(p.BaseStats.ToString())
|
||||
.WithThumbnailUrl($"https://assets.pokemon.com/assets/cms2/img/pokedex/detail/{p.Id.ToString("000")}.png")
|
||||
.AddField(GetText(strs.types), string.Join("\n", p.Types), true)
|
||||
.AddField(GetText(strs.height_weight), GetText(strs.height_weight_val(p.HeightM, p.WeightKg)), true)
|
||||
.AddField(GetText(strs.abilities), string.Join("\n", p.Abilities.Select(a => a.Value)), true));
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(kvp.Key.ToTitleCase())
|
||||
.WithDescription(p.BaseStats.ToString())
|
||||
.WithThumbnailUrl(
|
||||
$"https://assets.pokemon.com/assets/cms2/img/pokedex/detail/{p.Id.ToString("000")}.png")
|
||||
.AddField(GetText(strs.types), string.Join("\n", p.Types), true)
|
||||
.AddField(GetText(strs.height_weight),
|
||||
GetText(strs.height_weight_val(p.HeightM, p.WeightKg)),
|
||||
true)
|
||||
.AddField(GetText(strs.abilities),
|
||||
string.Join("\n", p.Abilities.Select(a => a.Value)),
|
||||
true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync(strs.pokemon_none);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task PokemonAbility([Leftover] string ability = null)
|
||||
{
|
||||
ability = ability?.Trim().ToUpperInvariant().Replace(" ", "", StringComparison.InvariantCulture);
|
||||
if (string.IsNullOrWhiteSpace(ability))
|
||||
return;
|
||||
foreach (var kvp in PokemonAbilities)
|
||||
{
|
||||
if (kvp.Key.ToUpperInvariant() == ability)
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithTitle(kvp.Value.Name)
|
||||
.WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc)
|
||||
? kvp.Value.ShortDesc
|
||||
: kvp.Value.Desc)
|
||||
.AddField(GetText(strs.rating), kvp.Value.Rating.ToString(Culture), true));
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(kvp.Value.Name)
|
||||
.WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc)
|
||||
? kvp.Value.ShortDesc
|
||||
: kvp.Value.Desc)
|
||||
.AddField(GetText(strs.rating),
|
||||
kvp.Value.Rating.ToString(Culture),
|
||||
true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync(strs.pokemon_ability_none);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Html.Dom;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using Newtonsoft.Json;
|
||||
@@ -11,20 +12,25 @@ using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Net;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Configuration = AngleSharp.Configuration;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, string> cachedShortenedLinks = new();
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IGoogleApiService _google;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly GuildTimezoneService _tzSvc;
|
||||
|
||||
public Searches(IBotCredentials creds, IGoogleApiService google, IHttpClientFactory factory, IMemoryCache cache,
|
||||
public Searches(
|
||||
IBotCredentials creds,
|
||||
IGoogleApiService google,
|
||||
IHttpClientFactory factory,
|
||||
IMemoryCache cache,
|
||||
GuildTimezoneService tzSvc)
|
||||
{
|
||||
_creds = creds;
|
||||
@@ -34,21 +40,21 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
_tzSvc = tzSvc;
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Rip([Leftover] IGuildUser usr)
|
||||
{
|
||||
var av = usr.RealAvatarUrl();
|
||||
if (av is null)
|
||||
return;
|
||||
await using var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av);
|
||||
await ctx.Channel.SendFileAsync(
|
||||
picStream,
|
||||
"rip.png",
|
||||
$"Rip {Format.Bold(usr.ToString())} \n\t- " +
|
||||
Format.Italics(ctx.User.ToString()));
|
||||
await ctx.Channel.SendFileAsync(picStream,
|
||||
"rip.png",
|
||||
$"Rip {Format.Bold(usr.ToString())} \n\t- " + Format.Italics(ctx.User.ToString()));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Weather([Leftover] string query)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -59,38 +65,47 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
embed.WithDescription(GetText(strs.city_not_found))
|
||||
.WithErrorColor();
|
||||
embed.WithDescription(GetText(strs.city_not_found)).WithErrorColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
var f = StandardConversions.CelsiusToFahrenheit;
|
||||
|
||||
var tz = ctx.Guild is null
|
||||
? TimeZoneInfo.Utc
|
||||
: _tzSvc.GetTimeZoneOrUtc(ctx.Guild.Id);
|
||||
|
||||
var tz = ctx.Guild is null ? TimeZoneInfo.Utc : _tzSvc.GetTimeZoneOrUtc(ctx.Guild.Id);
|
||||
var sunrise = data.Sys.Sunrise.ToUnixTimestamp();
|
||||
var sunset = data.Sys.Sunset.ToUnixTimestamp();
|
||||
sunrise = sunrise.ToOffset(tz.GetUtcOffset(sunrise));
|
||||
sunset = sunset.ToOffset(tz.GetUtcOffset(sunset));
|
||||
var timezone = $"UTC{sunrise:zzz}";
|
||||
|
||||
embed.AddField("🌍 " + Format.Bold(GetText(strs.location)), $"[{data.Name + ", " + data.Sys.Country}](https://openweathermap.org/city/{data.Id})", true)
|
||||
embed
|
||||
.AddField("🌍 " + Format.Bold(GetText(strs.location)),
|
||||
$"[{data.Name + ", " + data.Sys.Country}](https://openweathermap.org/city/{data.Id})",
|
||||
true)
|
||||
.AddField("📏 " + Format.Bold(GetText(strs.latlong)), $"{data.Coord.Lat}, {data.Coord.Lon}", true)
|
||||
.AddField("☁ " + Format.Bold(GetText(strs.condition)), string.Join(", ", data.Weather.Select(w => w.Main)), true)
|
||||
.AddField("☁ " + Format.Bold(GetText(strs.condition)),
|
||||
string.Join(", ", data.Weather.Select(w => w.Main)),
|
||||
true)
|
||||
.AddField("😓 " + Format.Bold(GetText(strs.humidity)), $"{data.Main.Humidity}%", true)
|
||||
.AddField("💨 " + Format.Bold(GetText(strs.wind_speed)), data.Wind.Speed + " m/s", true)
|
||||
.AddField("🌡 " + Format.Bold(GetText(strs.temperature)), $"{data.Main.Temp:F1}°C / {f(data.Main.Temp):F1}°F", true)
|
||||
.AddField("🔆 " + Format.Bold(GetText(strs.min_max)), $"{data.Main.TempMin:F1}°C - {data.Main.TempMax:F1}°C\n{f(data.Main.TempMin):F1}°F - {f(data.Main.TempMax):F1}°F", true)
|
||||
.AddField("🌡 " + Format.Bold(GetText(strs.temperature)),
|
||||
$"{data.Main.Temp:F1}°C / {f(data.Main.Temp):F1}°F",
|
||||
true)
|
||||
.AddField("🔆 " + Format.Bold(GetText(strs.min_max)),
|
||||
$"{data.Main.TempMin:F1}°C - {data.Main.TempMax:F1}°C\n{f(data.Main.TempMin):F1}°F - {f(data.Main.TempMax):F1}°F",
|
||||
true)
|
||||
.AddField("🌄 " + Format.Bold(GetText(strs.sunrise)), $"{sunrise:HH:mm} {timezone}", true)
|
||||
.AddField("🌇 " + Format.Bold(GetText(strs.sunset)), $"{sunset:HH:mm} {timezone}", true)
|
||||
.WithOkColor()
|
||||
.WithFooter("Powered by openweathermap.org", $"http://openweathermap.org/img/w/{data.Weather[0].Icon}.png");
|
||||
.WithFooter("Powered by openweathermap.org",
|
||||
$"http://openweathermap.org/img/w/{data.Weather[0].Icon}.png");
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Time([Leftover] string query)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -117,26 +132,29 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
errorKey = strs.error_occured;
|
||||
break;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync(errorKey);
|
||||
return;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(data.TimeZoneName))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.TimeZoneName))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.timezone_db_api_key);
|
||||
return;
|
||||
}
|
||||
|
||||
var eb = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString()))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString()))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
|
||||
await ctx.Channel.SendMessageAsync(embed: eb.Build());
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Youtube([Leftover] string query = null)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -152,7 +170,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ctx.Channel.SendMessageAsync(result);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Movie([Leftover] string query = null)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -166,37 +185,46 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ReplyErrorLocalizedAsync(strs.imdb_fail);
|
||||
return;
|
||||
}
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"http://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(movie.Poster));
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"http://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(movie.Poster));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public Task RandomCat() => InternalRandomImage(SearchesService.ImageTag.Cats);
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public Task RandomCat()
|
||||
=> InternalRandomImage(SearchesService.ImageTag.Cats);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public Task RandomDog() => InternalRandomImage(SearchesService.ImageTag.Dogs);
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public Task RandomDog()
|
||||
=> InternalRandomImage(SearchesService.ImageTag.Dogs);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public Task RandomFood() => InternalRandomImage(SearchesService.ImageTag.Food);
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public Task RandomFood()
|
||||
=> InternalRandomImage(SearchesService.ImageTag.Food);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public Task RandomBird() => InternalRandomImage(SearchesService.ImageTag.Birds);
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public Task RandomBird()
|
||||
=> InternalRandomImage(SearchesService.ImageTag.Birds);
|
||||
|
||||
private Task InternalRandomImage(SearchesService.ImageTag tag)
|
||||
{
|
||||
var url = _service.GetRandomImageUrl(tag);
|
||||
return ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithImageUrl(url));
|
||||
return ctx.Channel.EmbedAsync(_eb.Create().WithOkColor().WithImageUrl(url));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Image([Leftover] string query = null)
|
||||
{
|
||||
var oterms = query?.Trim();
|
||||
@@ -207,20 +235,20 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
var res = await _google.GetImageAsync(oterms);
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
|
||||
"http://i.imgur.com/G46fm8J.png",
|
||||
$"https://www.google.rs/search?q={query}&source=lnms&tbm=isch")
|
||||
.WithDescription(res.Link)
|
||||
.WithImageUrl(res.Link)
|
||||
.WithTitle(ctx.User.ToString());
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
|
||||
"http://i.imgur.com/G46fm8J.png",
|
||||
$"https://www.google.rs/search?q={query}&source=lnms&tbm=isch")
|
||||
.WithDescription(res.Link)
|
||||
.WithImageUrl(res.Link)
|
||||
.WithTitle(ctx.User.ToString());
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Falling back to Imgur");
|
||||
|
||||
var fullQueryLink = $"http://imgur.com/search?q={ query }";
|
||||
var fullQueryLink = $"http://imgur.com/search?q={query}";
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
using var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink);
|
||||
var elems = document.QuerySelectorAll("a.image-list-link").ToList();
|
||||
@@ -228,7 +256,9 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
if (!elems.Any())
|
||||
return;
|
||||
|
||||
var img = elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Count))?.Children?.FirstOrDefault() as IHtmlImageElement;
|
||||
var img =
|
||||
elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Count))?.Children?.FirstOrDefault() as
|
||||
IHtmlImageElement;
|
||||
|
||||
if (img?.Source is null)
|
||||
return;
|
||||
@@ -236,18 +266,19 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
var source = img.Source.Replace("b.", ".", StringComparison.InvariantCulture);
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
|
||||
"http://s.imgur.com/images/logo-1200-630.jpg?",
|
||||
fullQueryLink)
|
||||
.WithDescription(source)
|
||||
.WithImageUrl(source)
|
||||
.WithTitle(ctx.User.ToString());
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
|
||||
"http://s.imgur.com/images/logo-1200-630.jpg?",
|
||||
fullQueryLink)
|
||||
.WithDescription(source)
|
||||
.WithImageUrl(source)
|
||||
.WithTitle(ctx.User.ToString());
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Lmgtfy([Leftover] string ffs = null)
|
||||
{
|
||||
if (!await ValidateQuery(ffs))
|
||||
@@ -257,15 +288,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await SendConfirmAsync($"<{shortenedUrl}>");
|
||||
}
|
||||
|
||||
public class ShortenData
|
||||
{
|
||||
[JsonProperty("result_url")]
|
||||
public string ResultUrl { get; set; }
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string> cachedShortenedLinks = new();
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Shorten([Leftover] string query)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -273,15 +297,11 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
query = query.Trim();
|
||||
if (!cachedShortenedLinks.TryGetValue(query, out var 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" }
|
||||
};
|
||||
var formData = new MultipartFormDataContent { { new StringContent(query), "url" } };
|
||||
req.Content = formData;
|
||||
|
||||
using var res = await _http.SendAsync(req);
|
||||
@@ -300,15 +320,15 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
Log.Error(ex, "Error shortening a link: {Message}", ex.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"));
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Google([Leftover] string query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
@@ -316,32 +336,32 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
|
||||
var data = await _service.GoogleSearchAsync(query);
|
||||
if (data is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.no_results);
|
||||
return;
|
||||
}
|
||||
|
||||
var desc = data.Results.Take(5).Select(res =>
|
||||
$@"[**{res.Title}**]({res.Link})
|
||||
|
||||
var desc = data.Results.Take(5)
|
||||
.Select(res => $@"[**{res.Title}**]({res.Link})
|
||||
{res.Text.TrimTo(400 - res.Title.Length - res.Link.Length)}");
|
||||
|
||||
var descStr = string.Join("\n\n", desc);
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithAuthor(ctx.User.ToString(),
|
||||
iconUrl: "http://i.imgur.com/G46fm8J.png")
|
||||
.WithTitle(ctx.User.ToString())
|
||||
.WithFooter(data.TotalResults)
|
||||
.WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" +descStr)
|
||||
.WithOkColor();
|
||||
.WithAuthor(ctx.User.ToString(), "http://i.imgur.com/G46fm8J.png")
|
||||
.WithTitle(ctx.User.ToString())
|
||||
.WithFooter(data.TotalResults)
|
||||
.WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
|
||||
.WithOkColor();
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task DuckDuckGo([Leftover] string query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
@@ -349,30 +369,31 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
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})
|
||||
|
||||
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(),
|
||||
iconUrl: "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
|
||||
.WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
|
||||
.WithOkColor();
|
||||
.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);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task MagicTheGathering([Leftover] string search)
|
||||
{
|
||||
if (!await ValidateQuery(search))
|
||||
@@ -387,18 +408,20 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = _eb.Create().WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Hearthstone([Leftover] string name)
|
||||
{
|
||||
var arg = name;
|
||||
@@ -419,8 +442,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ReplyErrorLocalizedAsync(strs.card_not_found);
|
||||
return;
|
||||
}
|
||||
var embed = _eb.Create().WithOkColor()
|
||||
.WithImageUrl(card.Img);
|
||||
|
||||
var embed = _eb.Create().WithOkColor().WithImageUrl(card.Img);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(card.Flavor))
|
||||
embed.WithDescription(card.Flavor);
|
||||
@@ -428,7 +451,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task UrbanDict([Leftover] string query = null)
|
||||
{
|
||||
if (!await ValidateQuery(query))
|
||||
@@ -437,21 +461,25 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
var res = await http.GetStringAsync($"http://api.urbandictionary.com/v0/define?term={Uri.EscapeDataString(query)}");
|
||||
var res = await http.GetStringAsync(
|
||||
$"http://api.urbandictionary.com/v0/define?term={Uri.EscapeDataString(query)}");
|
||||
try
|
||||
{
|
||||
var items = JsonConvert.DeserializeObject<UrbanResponse>(res).List;
|
||||
if (items.Any())
|
||||
{
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(0, p =>
|
||||
{
|
||||
var item = items[p];
|
||||
return _eb.Create().WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithAuthor(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
}, items.Length, 1);
|
||||
await ctx.SendPaginatedConfirmAsync(0,
|
||||
p =>
|
||||
{
|
||||
var item = items[p];
|
||||
return _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithAuthor(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
},
|
||||
items.Length,
|
||||
1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -459,11 +487,12 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
}
|
||||
}
|
||||
await ReplyErrorLocalizedAsync(strs.ud_error);
|
||||
|
||||
await ReplyErrorLocalizedAsync(strs.ud_error);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Define([Leftover] string word)
|
||||
{
|
||||
if (!await ValidateQuery(word))
|
||||
@@ -473,17 +502,21 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
string res;
|
||||
try
|
||||
{
|
||||
res = await _cache.GetOrCreateAsync($"define_{word}", e =>
|
||||
{
|
||||
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
|
||||
return _http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword=" + WebUtility.UrlEncode(word));
|
||||
});
|
||||
res = await _cache.GetOrCreateAsync($"define_{word}",
|
||||
e =>
|
||||
{
|
||||
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
|
||||
return _http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword="
|
||||
+ WebUtility.UrlEncode(word));
|
||||
});
|
||||
|
||||
var data = JsonConvert.DeserializeObject<DefineModel>(res);
|
||||
|
||||
var datas = data.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));
|
||||
.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));
|
||||
|
||||
if (!datas.Any())
|
||||
{
|
||||
@@ -493,33 +526,35 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
|
||||
var col = datas.Select(data => (
|
||||
Definition: data.Sense.Definition is string
|
||||
? data.Sense.Definition.ToString()
|
||||
: ((JArray)JToken.Parse(data.Sense.Definition.ToString())).First.ToString(),
|
||||
Example: data.Sense.Examples is null || data.Sense.Examples.Count == 0
|
||||
? string.Empty
|
||||
: data.Sense.Examples[0].Text,
|
||||
Word: word,
|
||||
WordType: string.IsNullOrWhiteSpace(data.PartOfSpeech) ? "-" : data.PartOfSpeech
|
||||
)).ToList();
|
||||
Definition: data.Sense.Definition is string
|
||||
? data.Sense.Definition.ToString()
|
||||
: ((JArray)JToken.Parse(data.Sense.Definition.ToString())).First.ToString(),
|
||||
Example: data.Sense.Examples is null || data.Sense.Examples.Count == 0
|
||||
? string.Empty
|
||||
: data.Sense.Examples[0].Text, Word: word,
|
||||
WordType: string.IsNullOrWhiteSpace(data.PartOfSpeech) ? "-" : data.PartOfSpeech))
|
||||
.ToList();
|
||||
|
||||
Log.Information($"Sending {col.Count} definition for: {word}");
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(0, page =>
|
||||
{
|
||||
var data = col.Skip(page).First();
|
||||
var embed = _eb.Create()
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), data.Word, true)
|
||||
.AddField(GetText(strs._class), data.WordType, true)
|
||||
.AddField(GetText(strs.definition), data.Definition)
|
||||
.WithOkColor();
|
||||
await ctx.SendPaginatedConfirmAsync(0,
|
||||
page =>
|
||||
{
|
||||
var data = col.Skip(page).First();
|
||||
var embed = _eb.Create()
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), data.Word, true)
|
||||
.AddField(GetText(strs._class), data.WordType, true)
|
||||
.AddField(GetText(strs.definition), data.Definition)
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.Example))
|
||||
embed.AddField(GetText(strs.example), data.Example);
|
||||
if (!string.IsNullOrWhiteSpace(data.Example))
|
||||
embed.AddField(GetText(strs.example), data.Example);
|
||||
|
||||
return embed;
|
||||
}, col.Count, 1);
|
||||
return embed;
|
||||
},
|
||||
col.Count,
|
||||
1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -527,7 +562,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Catfact()
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
@@ -540,7 +576,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
//done in 3.0
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Revav([Leftover] IGuildUser usr = null)
|
||||
{
|
||||
@@ -555,7 +592,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
//done in 3.0
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Revimg([Leftover] string imageLink = null)
|
||||
{
|
||||
imageLink = imageLink?.Trim() ?? "";
|
||||
@@ -565,7 +603,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}");
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Wiki([Leftover] string query = null)
|
||||
{
|
||||
query = query?.Trim();
|
||||
@@ -574,7 +613,9 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var result = 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));
|
||||
var result = 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));
|
||||
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
|
||||
if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl))
|
||||
await ReplyErrorLocalizedAsync(strs.wiki_page_not_found);
|
||||
@@ -582,32 +623,28 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ctx.Channel.SendMessageAsync(data.Query.Pages[0].FullUrl);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
public async Task Color(params SixLabors.ImageSharp.Color[] colors)
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Color(params Color[] colors)
|
||||
{
|
||||
if (!colors.Any())
|
||||
return;
|
||||
|
||||
var colorObjects = colors.Take(10)
|
||||
.ToArray();
|
||||
var colorObjects = colors.Take(10).ToArray();
|
||||
|
||||
using var img = new Image<Rgba32>(colorObjects.Length * 50, 50);
|
||||
for (var i = 0; i < colorObjects.Length; i++)
|
||||
{
|
||||
var x = i * 50;
|
||||
img.Mutate(m => m.FillPolygon(colorObjects[i], new PointF[] {
|
||||
new(x, 0),
|
||||
new(x + 50, 0),
|
||||
new(x + 50, 50),
|
||||
new(x, 50)
|
||||
}));
|
||||
img.Mutate(m => m.FillPolygon(colorObjects[i], new(x, 0), new(x + 50, 0), new(x + 50, 50), new(x, 50)));
|
||||
}
|
||||
|
||||
await using var ms = img.ToStream();
|
||||
await ctx.Channel.SendFileAsync(ms, $"colors.png");
|
||||
await ctx.Channel.SendFileAsync(ms, "colors.png");
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Avatar([Leftover] IGuildUser usr = null)
|
||||
{
|
||||
@@ -622,13 +659,17 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()), ctx.User.Mention);
|
||||
await ctx.Channel.EmbedAsync(
|
||||
_eb.Create()
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()),
|
||||
ctx.User.Mention);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Wikia(string target, [Leftover] string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query))
|
||||
@@ -636,20 +677,21 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await ReplyErrorLocalizedAsync(strs.wikia_input_error);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
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 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))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.wikia_error);
|
||||
@@ -667,7 +709,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Bible(string book, string chapterAndVerse)
|
||||
{
|
||||
@@ -675,27 +718,30 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
try
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var res = await http
|
||||
.GetStringAsync("https://bible-api.com/" + book + " " + chapterAndVerse);
|
||||
var res = await http.GetStringAsync("https://bible-api.com/" + book + " " + chapterAndVerse);
|
||||
|
||||
obj = JsonConvert.DeserializeObject<BibleVerses>(res);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (obj.Error != null || obj.Verses is null || obj.Verses.Length == 0)
|
||||
{
|
||||
await SendErrorAsync(obj.Error ?? "No verse found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var v = obj.Verses[0];
|
||||
await ctx.Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||
.WithDescription(v.Text));
|
||||
.WithOkColor()
|
||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||
.WithDescription(v.Text));
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Steam([Leftover] string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
@@ -725,12 +771,15 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
public async Task<bool> ValidateQuery(string query)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(query)) return true;
|
||||
|
||||
await ErrorLocalizedAsync(strs.specify_search_params);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShortenData
|
||||
{
|
||||
[JsonProperty("result_url")]
|
||||
public string ResultUrl { get; set; }
|
||||
}
|
||||
}
|
@@ -23,8 +23,8 @@ public class AnimeSearchService : INService
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
try
|
||||
{
|
||||
|
||||
var link = "https://aniapi.nadeko.bot/anime/" + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||
var link = "https://aniapi.nadeko.bot/anime/"
|
||||
+ Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||
link = link.ToLowerInvariant();
|
||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link);
|
||||
if (!ok)
|
||||
@@ -33,6 +33,7 @@ public class AnimeSearchService : INService
|
||||
{
|
||||
data = await http.GetStringAsync(link);
|
||||
}
|
||||
|
||||
await _cache.SetAnimeDataAsync(link, data);
|
||||
}
|
||||
|
||||
@@ -53,10 +54,8 @@ public class AnimeSearchService : INService
|
||||
query = query.Replace(" ", "-", StringComparison.InvariantCulture);
|
||||
try
|
||||
{
|
||||
var link = "https://www.novelupdates.com/series/" + Uri.EscapeDataString(query
|
||||
.Replace(" ", "-")
|
||||
.Replace("/", " ")
|
||||
);
|
||||
var link = "https://www.novelupdates.com/series/"
|
||||
+ Uri.EscapeDataString(query.Replace(" ", "-").Replace("/", " "));
|
||||
link = link.ToLowerInvariant();
|
||||
var (ok, data) = await _cache.TryGetNovelDataAsync(link);
|
||||
if (!ok)
|
||||
@@ -71,32 +70,24 @@ public class AnimeSearchService : INService
|
||||
var descElem = document.QuerySelector("div#editdescription > p");
|
||||
var desc = descElem.InnerHtml;
|
||||
|
||||
var genres = document.QuerySelector("div#seriesgenre").Children
|
||||
.Select(x => x as IHtmlAnchorElement)
|
||||
.Where(x => x != null)
|
||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
||||
.ToArray();
|
||||
var genres = document.QuerySelector("div#seriesgenre")
|
||||
.Children.Select(x => x as IHtmlAnchorElement)
|
||||
.Where(x => x != null)
|
||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
||||
.ToArray();
|
||||
|
||||
var authors = document
|
||||
.QuerySelector("div#showauthors")
|
||||
.Children
|
||||
.Select(x => x as IHtmlAnchorElement)
|
||||
.Where(x => x != null)
|
||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
||||
.ToArray();
|
||||
var authors = document.QuerySelector("div#showauthors")
|
||||
.Children.Select(x => x as IHtmlAnchorElement)
|
||||
.Where(x => x != null)
|
||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
||||
.ToArray();
|
||||
|
||||
var score = ((IHtmlSpanElement)document
|
||||
.QuerySelector("h5.seriesother > span.uvotes"))
|
||||
.InnerHtml;
|
||||
var score = ((IHtmlSpanElement)document.QuerySelector("h5.seriesother > span.uvotes")).InnerHtml;
|
||||
|
||||
var status = document
|
||||
.QuerySelector("div#editstatus")
|
||||
.InnerHtml;
|
||||
var title = document
|
||||
.QuerySelector("div.w-blog-content > div.seriestitlenu")
|
||||
.InnerHtml;
|
||||
var status = document.QuerySelector("div#editstatus").InnerHtml;
|
||||
var title = document.QuerySelector("div.w-blog-content > div.seriestitlenu").InnerHtml;
|
||||
|
||||
var obj = new NovelResult()
|
||||
var obj = new NovelResult
|
||||
{
|
||||
Description = desc,
|
||||
Authors = authors,
|
||||
@@ -105,11 +96,10 @@ public class AnimeSearchService : INService
|
||||
Link = link,
|
||||
Score = score,
|
||||
Status = status,
|
||||
Title = title,
|
||||
Title = title
|
||||
};
|
||||
|
||||
await _cache.SetNovelDataAsync(link,
|
||||
JsonConvert.SerializeObject(obj));
|
||||
await _cache.SetNovelDataAsync(link, JsonConvert.SerializeObject(obj));
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -129,8 +119,8 @@ public class AnimeSearchService : INService
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
try
|
||||
{
|
||||
|
||||
var link = "https://aniapi.nadeko.bot/manga/" + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||
var link = "https://aniapi.nadeko.bot/manga/"
|
||||
+ Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||
link = link.ToLowerInvariant();
|
||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link);
|
||||
if (!ok)
|
||||
@@ -139,6 +129,7 @@ public class AnimeSearchService : INService
|
||||
{
|
||||
data = await http.GetStringAsync(link);
|
||||
}
|
||||
|
||||
await _cache.SetAnimeDataAsync(link, data);
|
||||
}
|
||||
|
||||
@@ -150,4 +141,4 @@ public class AnimeSearchService : INService
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,7 +8,5 @@ namespace NadekoBot.Modules.Searches;
|
||||
public static class AtlExtensions
|
||||
{
|
||||
public static Task<AutoTranslateChannel> GetByChannelId(this IQueryable<AutoTranslateChannel> set, ulong channelId)
|
||||
=> set
|
||||
.Include(x => x.Users)
|
||||
.FirstOrDefaultAsyncEF(x => x.ChannelId == channelId);
|
||||
}
|
||||
=> set.Include(x => x.Users).FirstOrDefaultAsyncEF(x => x.ChannelId == channelId);
|
||||
}
|
@@ -9,7 +9,9 @@ public class CryptoService : INService
|
||||
private readonly IDataCache _cache;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly IBotCredentials _creds;
|
||||
|
||||
|
||||
private readonly SemaphoreSlim getCryptoLock = new(1, 1);
|
||||
|
||||
public CryptoService(IDataCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
|
||||
{
|
||||
_cache = cache;
|
||||
@@ -19,70 +21,65 @@ public class CryptoService : INService
|
||||
|
||||
public async Task<(CryptoResponseData Data, CryptoResponseData Nearest)> GetCryptoData(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(name)) return (null, null);
|
||||
|
||||
name = name.ToUpperInvariant();
|
||||
var cryptos = await CryptoData();
|
||||
|
||||
if (cryptos is null)
|
||||
return (null, null);
|
||||
|
||||
var crypto = cryptos
|
||||
?.FirstOrDefault(x => x.Id.ToUpperInvariant() == name || x.Name.ToUpperInvariant() == name
|
||||
|| x.Symbol.ToUpperInvariant() == name);
|
||||
|
||||
var crypto = cryptos?.FirstOrDefault(x
|
||||
=> x.Id.ToUpperInvariant() == name
|
||||
|| x.Name.ToUpperInvariant() == name
|
||||
|| x.Symbol.ToUpperInvariant() == name);
|
||||
|
||||
(CryptoResponseData Elem, int Distance)? nearest = null;
|
||||
if (crypto is null)
|
||||
{
|
||||
nearest = cryptos
|
||||
.Select(x => (x, Distance: x.Name.ToUpperInvariant().LevenshteinDistance(name)))
|
||||
.OrderBy(x => x.Distance)
|
||||
.Where(x => x.Distance <= 2)
|
||||
.FirstOrDefault();
|
||||
nearest = cryptos.Select(x => (x, Distance: x.Name.ToUpperInvariant().LevenshteinDistance(name)))
|
||||
.OrderBy(x => x.Distance)
|
||||
.Where(x => x.Distance <= 2)
|
||||
.FirstOrDefault();
|
||||
|
||||
crypto = nearest?.Elem;
|
||||
}
|
||||
|
||||
if (nearest != null)
|
||||
{
|
||||
return (null, crypto);
|
||||
}
|
||||
if (nearest != null) return (null, crypto);
|
||||
|
||||
return (crypto, null);
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim getCryptoLock = new(1, 1);
|
||||
public async Task<List<CryptoResponseData>> CryptoData()
|
||||
{
|
||||
await getCryptoLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var fullStrData = await _cache.GetOrAddCachedDataAsync("nadeko:crypto_data", async _ =>
|
||||
{
|
||||
try
|
||||
var fullStrData = await _cache.GetOrAddCachedDataAsync("nadeko:crypto_data",
|
||||
async _ =>
|
||||
{
|
||||
using var _http = _httpFactory.CreateClient();
|
||||
var strData = await _http.GetStringAsync(
|
||||
$"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?" +
|
||||
$"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}" +
|
||||
$"&start=1" +
|
||||
$"&limit=5000" +
|
||||
$"&convert=USD");
|
||||
try
|
||||
{
|
||||
using var _http = _httpFactory.CreateClient();
|
||||
var strData = await _http.GetStringAsync(
|
||||
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?"
|
||||
+ $"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}"
|
||||
+ "&start=1"
|
||||
+ "&limit=5000"
|
||||
+ "&convert=USD");
|
||||
|
||||
JsonConvert.DeserializeObject<CryptoResponse>(strData); // just to see if its' valid
|
||||
JsonConvert.DeserializeObject<CryptoResponse>(strData); // just to see if its' valid
|
||||
|
||||
return strData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting crypto data: {Message}", ex.Message);
|
||||
return default;
|
||||
}
|
||||
|
||||
}, "", TimeSpan.FromHours(1));
|
||||
return strData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting crypto data: {Message}", ex.Message);
|
||||
return default;
|
||||
}
|
||||
},
|
||||
"",
|
||||
TimeSpan.FromHours(1));
|
||||
|
||||
return JsonConvert.DeserializeObject<CryptoResponse>(fullStrData).Data;
|
||||
}
|
||||
@@ -96,4 +93,4 @@ public class CryptoService : INService
|
||||
getCryptoLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
#nullable disable
|
||||
using CodeHollow.FeedReader;
|
||||
using CodeHollow.FeedReader.Feeds;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
@@ -15,22 +16,25 @@ public class FeedsService : INService
|
||||
|
||||
private readonly ConcurrentDictionary<string, DateTime> _lastPosts = new();
|
||||
|
||||
public FeedsService(Bot bot, DbService db, DiscordSocketClient client, IEmbedBuilderService eb)
|
||||
public FeedsService(
|
||||
Bot bot,
|
||||
DbService db,
|
||||
DiscordSocketClient client,
|
||||
IEmbedBuilderService eb)
|
||||
{
|
||||
_db = db;
|
||||
|
||||
using (var uow = db.GetDbContext())
|
||||
{
|
||||
var guildConfigIds = bot.AllGuildConfigs.Select(x => x.Id).ToList();
|
||||
_subs = uow.GuildConfigs
|
||||
.AsQueryable()
|
||||
.Where(x => guildConfigIds.Contains(x.Id))
|
||||
.Include(x => x.FeedSubs)
|
||||
.ToList()
|
||||
.SelectMany(x => x.FeedSubs)
|
||||
.GroupBy(x => x.Url.ToLower())
|
||||
.ToDictionary(x => x.Key, x => x.ToHashSet())
|
||||
.ToConcurrent();
|
||||
_subs = uow.GuildConfigs.AsQueryable()
|
||||
.Where(x => guildConfigIds.Contains(x.Id))
|
||||
.Include(x => x.FeedSubs)
|
||||
.ToList()
|
||||
.SelectMany(x => x.FeedSubs)
|
||||
.GroupBy(x => x.Url.ToLower())
|
||||
.ToDictionary(x => x.Key, x => x.ToHashSet())
|
||||
.ToConcurrent();
|
||||
}
|
||||
|
||||
_client = client;
|
||||
@@ -52,34 +56,27 @@ public class FeedsService : INService
|
||||
var rssUrl = kvp.Key;
|
||||
try
|
||||
{
|
||||
var feed = await CodeHollow.FeedReader.FeedReader.ReadAsync(rssUrl);
|
||||
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();
|
||||
.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();
|
||||
|
||||
if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate))
|
||||
{
|
||||
lastFeedUpdate = _lastPosts[kvp.Key] =
|
||||
items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow;
|
||||
}
|
||||
|
||||
foreach (var (feedItem, itemUpdateDate) in items)
|
||||
{
|
||||
if (itemUpdateDate <= lastFeedUpdate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (itemUpdateDate <= lastFeedUpdate) continue;
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithFooter(rssUrl);
|
||||
var embed = _eb.Create().WithFooter(rssUrl);
|
||||
|
||||
_lastPosts[kvp.Key] = itemUpdateDate;
|
||||
|
||||
@@ -87,40 +84,36 @@ public class FeedsService : INService
|
||||
if (!string.IsNullOrWhiteSpace(link) && Uri.IsWellFormedUriString(link, UriKind.Absolute))
|
||||
embed.WithUrl(link);
|
||||
|
||||
var title = string.IsNullOrWhiteSpace(feedItem.Title)
|
||||
? "-"
|
||||
: feedItem.Title;
|
||||
var title = string.IsNullOrWhiteSpace(feedItem.Title) ? "-" : feedItem.Title;
|
||||
|
||||
var gotImage = false;
|
||||
if (feedItem.SpecificItem is MediaRssFeedItem mrfi &&
|
||||
(mrfi.Enclosure?.MediaType?.StartsWith("image/") ?? false))
|
||||
if (feedItem.SpecificItem is MediaRssFeedItem mrfi
|
||||
&& (mrfi.Enclosure?.MediaType?.StartsWith("image/") ?? false))
|
||||
{
|
||||
var imgUrl = mrfi.Enclosure.Url;
|
||||
if (!string.IsNullOrWhiteSpace(imgUrl) &&
|
||||
Uri.IsWellFormedUriString(imgUrl, UriKind.Absolute))
|
||||
if (!string.IsNullOrWhiteSpace(imgUrl)
|
||||
&& Uri.IsWellFormedUriString(imgUrl, UriKind.Absolute))
|
||||
{
|
||||
embed.WithImageUrl(imgUrl);
|
||||
gotImage = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!gotImage && feedItem.SpecificItem is AtomFeedItem afi)
|
||||
{
|
||||
var previewElement = afi.Element.Elements()
|
||||
.FirstOrDefault(x => x.Name.LocalName == "preview");
|
||||
.FirstOrDefault(x => x.Name.LocalName == "preview");
|
||||
|
||||
if (previewElement is null)
|
||||
{
|
||||
previewElement = afi.Element.Elements()
|
||||
.FirstOrDefault(x => x.Name.LocalName == "thumbnail");
|
||||
}
|
||||
|
||||
.FirstOrDefault(x => x.Name.LocalName == "thumbnail");
|
||||
|
||||
if (previewElement != null)
|
||||
{
|
||||
var urlAttribute = previewElement.Attribute("url");
|
||||
if (urlAttribute != null && !string.IsNullOrWhiteSpace(urlAttribute.Value)
|
||||
&& Uri.IsWellFormedUriString(urlAttribute.Value,
|
||||
UriKind.Absolute))
|
||||
if (urlAttribute != null
|
||||
&& !string.IsNullOrWhiteSpace(urlAttribute.Value)
|
||||
&& Uri.IsWellFormedUriString(urlAttribute.Value, UriKind.Absolute))
|
||||
{
|
||||
embed.WithImageUrl(urlAttribute.Value);
|
||||
gotImage = true;
|
||||
@@ -136,12 +129,11 @@ public class FeedsService : INService
|
||||
embed.WithDescription(desc.TrimTo(2048));
|
||||
|
||||
//send the created embed to all subscribed channels
|
||||
var feedSendTasks = kvp.Value
|
||||
.Where(x => x.GuildConfig != null)
|
||||
.Select(x => _client.GetGuild(x.GuildConfig.GuildId)
|
||||
?.GetTextChannel(x.ChannelId))
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.EmbedAsync(embed));
|
||||
var feedSendTasks = kvp.Value.Where(x => x.GuildConfig != null)
|
||||
.Select(x => _client.GetGuild(x.GuildConfig.GuildId)
|
||||
?.GetTextChannel(x.ChannelId))
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.EmbedAsync(embed));
|
||||
|
||||
allSendTasks.Add(feedSendTasks.WhenAll());
|
||||
}
|
||||
@@ -158,49 +150,35 @@ public class FeedsService : INService
|
||||
public List<FeedSub> GetFeeds(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.GuildConfigsForId(guildId,
|
||||
set => set.Include(x => x.FeedSubs)
|
||||
.ThenInclude(x => x.GuildConfig))
|
||||
.FeedSubs
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
return uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs).ThenInclude(x => x.GuildConfig))
|
||||
.FeedSubs.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public bool AddFeed(ulong guildId, ulong channelId, string rssFeed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rssFeed, nameof(rssFeed));
|
||||
|
||||
var fs = new FeedSub()
|
||||
{
|
||||
ChannelId = channelId,
|
||||
Url = rssFeed.Trim(),
|
||||
};
|
||||
var fs = new FeedSub { ChannelId = channelId, Url = rssFeed.Trim() };
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
var gc = uow.GuildConfigsForId(guildId,
|
||||
set => set.Include(x => x.FeedSubs)
|
||||
.ThenInclude(x => x.GuildConfig));
|
||||
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs).ThenInclude(x => x.GuildConfig));
|
||||
|
||||
if (gc.FeedSubs.Any(x => x.Url.ToLower() == fs.Url.ToLower()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (gc.FeedSubs.Count >= 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (gc.FeedSubs.Count >= 10) return false;
|
||||
|
||||
gc.FeedSubs.Add(fs);
|
||||
uow.SaveChanges();
|
||||
//adding all, in case bot wasn't on this guild when it started
|
||||
foreach (var feed in gc.FeedSubs)
|
||||
{
|
||||
_subs.AddOrUpdate(feed.Url.ToLower(), new HashSet<FeedSub>() {feed}, (k, old) =>
|
||||
{
|
||||
old.Add(feed);
|
||||
return old;
|
||||
});
|
||||
}
|
||||
_subs.AddOrUpdate(feed.Url.ToLower(),
|
||||
new HashSet<FeedSub> { feed },
|
||||
(k, old) =>
|
||||
{
|
||||
old.Add(feed);
|
||||
return old;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -212,21 +190,22 @@ public class FeedsService : INService
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
var items = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FeedSubs))
|
||||
.FeedSubs
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
.FeedSubs.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (items.Count <= index)
|
||||
return false;
|
||||
var toRemove = items[index];
|
||||
_subs.AddOrUpdate(toRemove.Url.ToLower(), new HashSet<FeedSub>(), (key, old) =>
|
||||
{
|
||||
old.Remove(toRemove);
|
||||
return old;
|
||||
});
|
||||
_subs.AddOrUpdate(toRemove.Url.ToLower(),
|
||||
new HashSet<FeedSub>(),
|
||||
(key, old) =>
|
||||
{
|
||||
old.Remove(toRemove);
|
||||
return old;
|
||||
});
|
||||
uow.Remove(toRemove);
|
||||
uow.SaveChanges();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,12 @@ public interface ITranslateService
|
||||
public Task<string> Translate(string source, string target, string text = null);
|
||||
Task<bool> ToggleAtl(ulong guildId, ulong channelId, bool autoDelete);
|
||||
IEnumerable<string> GetLanguages();
|
||||
Task<bool?> RegisterUserAsync(ulong userId, ulong channelId, string @from, string to);
|
||||
|
||||
Task<bool?> RegisterUserAsync(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
string from,
|
||||
string to);
|
||||
|
||||
Task<bool> UnregisterUser(ulong channelId, ulong userId);
|
||||
}
|
||||
}
|
@@ -1,21 +1,42 @@
|
||||
#nullable disable
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using Html2Markdown;
|
||||
using NadekoBot.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 System.Net;
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using HorizontalAlignment = SixLabors.Fonts.HorizontalAlignment;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
public class SearchesService : INService
|
||||
{
|
||||
public enum ImageTag
|
||||
{
|
||||
Food,
|
||||
Dogs,
|
||||
Cats,
|
||||
Birds
|
||||
}
|
||||
|
||||
private static readonly HtmlParser _googleParser = new(new()
|
||||
{
|
||||
IsScripting = false,
|
||||
IsEmbedded = false,
|
||||
IsSupportingProcessingInstructions = false,
|
||||
IsKeepingSourceReferences = false,
|
||||
IsNotSupportingFrames = true
|
||||
});
|
||||
|
||||
public List<WoWJoke> WowJokes { get; } = new();
|
||||
public List<MagicItem> MagicItems { get; } = new();
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly IGoogleApiService _google;
|
||||
private readonly IImageCache _imgs;
|
||||
@@ -23,12 +44,13 @@ public class SearchesService : INService
|
||||
private readonly FontProvider _fonts;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
public List<WoWJoke> WowJokes { get; } = new();
|
||||
public List<MagicItem> MagicItems { get; } = new();
|
||||
private readonly List<string> _yomamaJokes;
|
||||
|
||||
public SearchesService(IGoogleApiService google,
|
||||
private readonly object yomamaLock = new();
|
||||
private int yomamaJokeIndex;
|
||||
|
||||
public SearchesService(
|
||||
IGoogleApiService google,
|
||||
IDataCache cache,
|
||||
IHttpClientFactory factory,
|
||||
FontProvider fonts,
|
||||
@@ -44,24 +66,18 @@ public class SearchesService : INService
|
||||
|
||||
//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();
|
||||
_yomamaJokes = File.ReadAllLines("data/yomama.txt").Shuffle().ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -94,12 +110,11 @@ public class SearchesService : INService
|
||||
data = await http.GetByteArrayAsync(avatarUrl);
|
||||
using (var avatarImg = Image.Load<Rgba32>(data))
|
||||
{
|
||||
avatarImg.Mutate(x => x
|
||||
.Resize(85, 85)
|
||||
.ApplyRoundedCorners(42));
|
||||
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
|
||||
data = avatarImg.ToStream().ToArray();
|
||||
DrawAvatar(bg, avatarImg);
|
||||
}
|
||||
|
||||
await _cache.SetImageDataAsync(avatarUrl, data);
|
||||
}
|
||||
else
|
||||
@@ -113,13 +128,12 @@ public class SearchesService : INService
|
||||
{
|
||||
TextOptions = new TextOptions
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
WrapTextWidth = 190,
|
||||
HorizontalAlignment = HorizontalAlignment.Center, WrapTextWidth = 190
|
||||
}.WithFallbackFonts(_fonts.FallBackFonts)
|
||||
},
|
||||
text,
|
||||
_fonts.RipFont,
|
||||
SixLabors.ImageSharp.Color.Black,
|
||||
Color.Black,
|
||||
new(25, 225)));
|
||||
|
||||
//flowa
|
||||
@@ -138,7 +152,7 @@ public class SearchesService : INService
|
||||
return _cache.GetOrAddCachedDataAsync($"nadeko_weather_{query}",
|
||||
GetWeatherDataFactory,
|
||||
query,
|
||||
expiry: TimeSpan.FromHours(3));
|
||||
TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
private async Task<WeatherData> GetWeatherDataFactory(string query)
|
||||
@@ -146,10 +160,10 @@ public class SearchesService : INService
|
||||
using var http = _httpFactory.CreateClient();
|
||||
try
|
||||
{
|
||||
var data = await http.GetStringAsync($"http://api.openweathermap.org/data/2.5/weather?" +
|
||||
$"q={query}&" +
|
||||
$"appid=42cd627dd60debf25a5739e50a217d74&" +
|
||||
$"units=metric");
|
||||
var data = await http.GetStringAsync("http://api.openweathermap.org/data/2.5/weather?"
|
||||
+ $"q={query}&"
|
||||
+ "appid=42cd627dd60debf25a5739e50a217d74&"
|
||||
+ "units=metric");
|
||||
|
||||
if (data is null)
|
||||
return null;
|
||||
@@ -170,34 +184,34 @@ public class SearchesService : INService
|
||||
// GetTimeDataFactory,
|
||||
// arg,
|
||||
// TimeSpan.FromMinutes(1));
|
||||
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory(string query)
|
||||
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory(
|
||||
string query)
|
||||
{
|
||||
query = query.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return (default, TimeErrors.InvalidInput);
|
||||
}
|
||||
if (string.IsNullOrEmpty(query)) return (default, TimeErrors.InvalidInput);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey)
|
||||
|| string.IsNullOrWhiteSpace(_creds.TimezoneDbApiKey))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey) || string.IsNullOrWhiteSpace(_creds.TimezoneDbApiKey))
|
||||
return (default, TimeErrors.ApiKeyMissing);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var _http = _httpFactory.CreateClient();
|
||||
var res = await _cache.GetOrAddCachedDataAsync($"geo_{query}", _ =>
|
||||
{
|
||||
var url = "https://eu1.locationiq.com/v1/search.php?" +
|
||||
(string.IsNullOrWhiteSpace(_creds.LocationIqApiKey) ? "key=" : $"key={_creds.LocationIqApiKey}&") +
|
||||
$"q={Uri.EscapeDataString(query)}&" +
|
||||
$"format=json";
|
||||
var res = await _cache.GetOrAddCachedDataAsync($"geo_{query}",
|
||||
_ =>
|
||||
{
|
||||
var url = "https://eu1.locationiq.com/v1/search.php?"
|
||||
+ (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey)
|
||||
? "key="
|
||||
: $"key={_creds.LocationIqApiKey}&")
|
||||
+ $"q={Uri.EscapeDataString(query)}&"
|
||||
+ "format=json";
|
||||
|
||||
var res = _http.GetStringAsync(url);
|
||||
return res;
|
||||
}, "", TimeSpan.FromHours(1));
|
||||
var res = _http.GetStringAsync(url);
|
||||
return res;
|
||||
},
|
||||
"",
|
||||
TimeSpan.FromHours(1));
|
||||
|
||||
var responses = JsonConvert.DeserializeObject<LocationIqResponse[]>(res);
|
||||
if (responses is null || responses.Length == 0)
|
||||
@@ -208,21 +222,18 @@ public class SearchesService : INService
|
||||
|
||||
var geoData = responses[0];
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "http://api.timezonedb.com/v2.1/get-time-zone?" +
|
||||
$"key={_creds.TimezoneDbApiKey}&format=json&" +
|
||||
"by=position&" +
|
||||
$"lat={geoData.Lat}&lng={geoData.Lon}");
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get,
|
||||
"http://api.timezonedb.com/v2.1/get-time-zone?"
|
||||
+ $"key={_creds.TimezoneDbApiKey}&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, System.DateTimeKind.Utc).AddSeconds(timeObj.Timestamp);
|
||||
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);
|
||||
return ((Address: responses[0].DisplayName, Time: time, TimeZoneName: timeObj.TimezoneName), default);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -231,14 +242,6 @@ public class SearchesService : INService
|
||||
}
|
||||
}
|
||||
|
||||
public enum ImageTag
|
||||
{
|
||||
Food,
|
||||
Dogs,
|
||||
Cats,
|
||||
Birds
|
||||
}
|
||||
|
||||
public string GetRandomImageUrl(ImageTag tag)
|
||||
{
|
||||
var subpath = tag.ToString().ToLowerInvariant();
|
||||
@@ -263,12 +266,11 @@ public class SearchesService : INService
|
||||
break;
|
||||
}
|
||||
|
||||
return $"https://nadeko-pictures.nyc3.digitaloceanspaces.com/{subpath}/" +
|
||||
_rng.Next(1, max).ToString("000") + ".png";
|
||||
return $"https://nadeko-pictures.nyc3.digitaloceanspaces.com/{subpath}/"
|
||||
+ _rng.Next(1, max).ToString("000")
|
||||
+ ".png";
|
||||
}
|
||||
|
||||
private readonly object yomamaLock = new();
|
||||
private int yomamaJokeIndex = 0;
|
||||
public Task<string> GetYomamaJoke()
|
||||
{
|
||||
string joke;
|
||||
@@ -284,8 +286,9 @@ public class SearchesService : INService
|
||||
|
||||
joke = _yomamaJokes[yomamaJokeIndex++];
|
||||
}
|
||||
|
||||
return Task.FromResult(joke);
|
||||
|
||||
|
||||
// using (var http = _httpFactory.CreateClient())
|
||||
// {
|
||||
// var response = await http.GetStringAsync(new Uri("http://api.yomomma.info/"));
|
||||
@@ -297,7 +300,7 @@ public class SearchesService : INService
|
||||
{
|
||||
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 = ""});
|
||||
var resObj = JsonConvert.DeserializeAnonymousType(res, new { setup = "", punchline = "" });
|
||||
return (resObj.setup, resObj.punchline);
|
||||
}
|
||||
|
||||
@@ -305,7 +308,7 @@ public class SearchesService : INService
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var response = await http.GetStringAsync(new Uri("http://api.icndb.com/jokes/random/"));
|
||||
return JObject.Parse(response)["value"]["joke"].ToString() + " 😆";
|
||||
return JObject.Parse(response)["value"]["joke"] + " 😆";
|
||||
}
|
||||
|
||||
public async Task<MtgData> GetMtgCardAsync(string search)
|
||||
@@ -329,11 +332,11 @@ public class SearchesService : INService
|
||||
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)}");
|
||||
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>"; }
|
||||
|
||||
@@ -344,13 +347,14 @@ public class SearchesService : INService
|
||||
ImageUrl = card.ImageUrl,
|
||||
StoreUrl = storeUrl,
|
||||
Types = string.Join(",\n", card.Types),
|
||||
ManaCost = card.ManaCost,
|
||||
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 response =
|
||||
await http.GetStringAsync($"https://api.magicthegathering.io/v1/cards?name={Uri.EscapeDataString(search)}");
|
||||
|
||||
var responseObject = JsonConvert.DeserializeObject<MtgResponse>(response);
|
||||
if (responseObject is null)
|
||||
@@ -360,8 +364,7 @@ public class SearchesService : INService
|
||||
if (cards.Length == 0)
|
||||
return Array.Empty<MtgData>();
|
||||
|
||||
return await cards.Select(GetMtgDataAsync)
|
||||
.WhenAll();
|
||||
return await cards.Select(GetMtgDataAsync).WhenAll();
|
||||
}
|
||||
|
||||
public Task<HearthstoneCardData> GetHearthstoneCardDataAsync(string name)
|
||||
@@ -380,25 +383,22 @@ public class SearchesService : INService
|
||||
http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.RapidApiKey);
|
||||
try
|
||||
{
|
||||
var response = await http.GetStringAsync($"https://omgvamp-hearthstone-v1.p.rapidapi.com/" +
|
||||
$"cards/search/{Uri.EscapeDataString(name)}");
|
||||
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();
|
||||
?? 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.Img)) data.Img = await _google.ShortenUrl(data.Img);
|
||||
if (!string.IsNullOrWhiteSpace(data.Text))
|
||||
{
|
||||
var converter = new Html2Markdown.Converter();
|
||||
var converter = new Converter();
|
||||
data.Text = converter.Convert(data.Text);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -411,10 +411,7 @@ public class SearchesService : INService
|
||||
public Task<OmdbMovie> GetMovieDataAsync(string name)
|
||||
{
|
||||
name = name.Trim().ToLowerInvariant();
|
||||
return _cache.GetOrAddCachedDataAsync($"nadeko_movie_{name}",
|
||||
GetMovieDataFactory,
|
||||
name,
|
||||
TimeSpan.FromDays(1));
|
||||
return _cache.GetOrAddCachedDataAsync($"nadeko_movie_{name}", GetMovieDataFactory, name, TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
private async Task<OmdbMovie> GetMovieDataFactory(string name)
|
||||
@@ -451,27 +448,30 @@ public class SearchesService : INService
|
||||
// }
|
||||
//}
|
||||
|
||||
var gamesMap = await _cache.GetOrAddCachedDataAsync(STEAM_GAME_IDS_KEY, 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;
|
||||
var gamesMap = await _cache.GetOrAddCachedDataAsync(STEAM_GAME_IDS_KEY,
|
||||
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);
|
||||
//await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray());
|
||||
//await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
|
||||
//await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget);
|
||||
}, default(string), TimeSpan.FromHours(24));
|
||||
return apps.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(x => x.Name)
|
||||
.ToDictionary(x => x.Key, x => x.First().AppId);
|
||||
//await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray());
|
||||
//await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
|
||||
//await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget);
|
||||
},
|
||||
default(string),
|
||||
TimeSpan.FromHours(24));
|
||||
|
||||
if (gamesMap is null)
|
||||
return -1;
|
||||
|
||||
|
||||
|
||||
query = query.Trim();
|
||||
|
||||
var keyList = gamesMap.Keys.ToList();
|
||||
@@ -503,6 +503,105 @@ public class SearchesService : INService
|
||||
//return gameData;
|
||||
}
|
||||
|
||||
public async Task<GoogleSearchResultData> GoogleSearchAsync(string query)
|
||||
{
|
||||
query = WebUtility.UrlEncode(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);
|
||||
var content = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
using var document = await _googleParser.ParseDocumentAsync(content);
|
||||
var elems = document.QuerySelectorAll("div.g > div > div");
|
||||
|
||||
var resultsElem = document.QuerySelectorAll("#resultStats").FirstOrDefault();
|
||||
var totalResults = resultsElem?.TextContent;
|
||||
//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 children = elem.Children.ToList();
|
||||
if (children.Count < 2)
|
||||
return null;
|
||||
|
||||
var href = (children[0].QuerySelector("a") as IHtmlAnchorElement)?.Href;
|
||||
var name = children[0].QuerySelector("h3")?.TextContent;
|
||||
|
||||
if (href is null || name is null)
|
||||
return null;
|
||||
|
||||
var txt = children[1].TextContent;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(txt))
|
||||
return null;
|
||||
|
||||
return new GoogleSearchResult(name, href, txt);
|
||||
})
|
||||
.Where(x => x != null)
|
||||
.ToList();
|
||||
|
||||
return new(results.AsReadOnly(), fullQueryLink, totalResults);
|
||||
}
|
||||
|
||||
public async Task<GoogleSearchResultData> DuckDuckGoSearchAsync(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 != null)
|
||||
.ToList();
|
||||
|
||||
return new(results.AsReadOnly(), fullQueryLink, "0");
|
||||
}
|
||||
|
||||
//private async Task<SteamGameData> SteamGameDataFactory(int appid)
|
||||
//{
|
||||
// using (var http = _httpFactory.CreateClient())
|
||||
@@ -523,7 +622,9 @@ public class SearchesService : INService
|
||||
public string FullQueryLink { get; }
|
||||
public string TotalResults { get; }
|
||||
|
||||
public GoogleSearchResultData(IReadOnlyList<GoogleSearchResult> results, string fullQueryLink,
|
||||
public GoogleSearchResultData(
|
||||
IReadOnlyList<GoogleSearchResult> results,
|
||||
string fullQueryLink,
|
||||
string totalResults)
|
||||
{
|
||||
Results = results;
|
||||
@@ -531,116 +632,4 @@ public class SearchesService : INService
|
||||
TotalResults = totalResults;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HtmlParser _googleParser = new(new()
|
||||
{
|
||||
IsScripting = false,
|
||||
IsEmbedded = false,
|
||||
IsSupportingProcessingInstructions = false,
|
||||
IsKeepingSourceReferences = false,
|
||||
IsNotSupportingFrames = true,
|
||||
});
|
||||
|
||||
public async Task<GoogleSearchResultData> GoogleSearchAsync(string query)
|
||||
{
|
||||
query = WebUtility.UrlEncode(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);
|
||||
var content = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
using var document = await _googleParser.ParseDocumentAsync(content);
|
||||
var elems = document.QuerySelectorAll("div.g > div > div");
|
||||
|
||||
var resultsElem = document.QuerySelectorAll("#resultStats").FirstOrDefault();
|
||||
var totalResults = resultsElem?.TextContent;
|
||||
//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 children = elem.Children.ToList();
|
||||
if (children.Count < 2)
|
||||
return null;
|
||||
|
||||
var href = (children[0].QuerySelector("a") as IHtmlAnchorElement)?.Href;
|
||||
var name = children[0].QuerySelector("h3")?.TextContent;
|
||||
|
||||
if (href is null || name is null)
|
||||
return null;
|
||||
|
||||
var txt = children[1].TextContent;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(txt))
|
||||
return null;
|
||||
|
||||
return new GoogleSearchResult(name, href, txt);
|
||||
})
|
||||
.Where(x => x != null)
|
||||
.ToList();
|
||||
|
||||
return new(
|
||||
results.AsReadOnly(),
|
||||
fullQueryLink,
|
||||
totalResults);
|
||||
}
|
||||
|
||||
public async Task<GoogleSearchResultData> DuckDuckGoSearchAsync(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 != null)
|
||||
.ToList();
|
||||
|
||||
return new(
|
||||
results.AsReadOnly(),
|
||||
fullQueryLink,
|
||||
"0");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Common.StreamNotifications;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
using StackExchange.Redis;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class StreamNotificationService : INService
|
||||
|
||||
private readonly Dictionary<StreamDataKey, Dictionary<ulong, HashSet<FollowedStream>>> _shardTrackedStreams;
|
||||
private readonly ConcurrentHashSet<ulong> _offlineNotificationServers;
|
||||
|
||||
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly IEmbedBuilderService _eb;
|
||||
@@ -31,7 +31,7 @@ public sealed class StreamNotificationService : INService
|
||||
|
||||
private readonly TypedKey<List<StreamData>> _streamsOnlineKey;
|
||||
private readonly TypedKey<List<StreamData>> _streamsOfflineKey;
|
||||
|
||||
|
||||
private readonly TypedKey<FollowStreamPubData> _streamFollowKey;
|
||||
private readonly TypedKey<FollowStreamPubData> _streamUnfollowKey;
|
||||
|
||||
@@ -64,45 +64,36 @@ public sealed class StreamNotificationService : INService
|
||||
{
|
||||
var ids = client.GetGuildIds();
|
||||
var guildConfigs = uow.Set<GuildConfig>()
|
||||
.AsQueryable()
|
||||
.Include(x => x.FollowedStreams)
|
||||
.Where(x => ids.Contains(x.GuildId))
|
||||
.ToList();
|
||||
.AsQueryable()
|
||||
.Include(x => x.FollowedStreams)
|
||||
.Where(x => ids.Contains(x.GuildId))
|
||||
.ToList();
|
||||
|
||||
_offlineNotificationServers = new(guildConfigs
|
||||
.Where(gc => gc.NotifyStreamOffline)
|
||||
.Select(x => x.GuildId)
|
||||
.ToList());
|
||||
.Where(gc => gc.NotifyStreamOffline)
|
||||
.Select(x => x.GuildId)
|
||||
.ToList());
|
||||
|
||||
var followedStreams = guildConfigs
|
||||
.SelectMany(x => x.FollowedStreams)
|
||||
.ToList();
|
||||
var followedStreams = guildConfigs.SelectMany(x => x.FollowedStreams).ToList();
|
||||
|
||||
_shardTrackedStreams = followedStreams
|
||||
.GroupBy(x => new {Type = x.Type, Name = x.Username.ToLower()})
|
||||
.ToList()
|
||||
.ToDictionary(
|
||||
x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()),
|
||||
x => x.GroupBy(y => y.GuildId)
|
||||
.ToDictionary(y => y.Key, y => y.AsEnumerable().ToHashSet()));
|
||||
_shardTrackedStreams = followedStreams.GroupBy(x => new { x.Type, Name = x.Username.ToLower() })
|
||||
.ToList()
|
||||
.ToDictionary(
|
||||
x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()),
|
||||
x => x.GroupBy(y => y.GuildId)
|
||||
.ToDictionary(y => y.Key,
|
||||
y => y.AsEnumerable().ToHashSet()));
|
||||
|
||||
// shard 0 will keep track of when there are no more guilds which track a stream
|
||||
if (client.ShardId == 0)
|
||||
{
|
||||
var allFollowedStreams = uow.Set<FollowedStream>()
|
||||
.AsQueryable()
|
||||
.ToList();
|
||||
var allFollowedStreams = uow.Set<FollowedStream>().AsQueryable().ToList();
|
||||
|
||||
foreach (var fs in allFollowedStreams)
|
||||
{
|
||||
_streamTracker.CacheAddData(fs.CreateKey(), null, replace: false);
|
||||
}
|
||||
foreach (var fs in allFollowedStreams) _streamTracker.CacheAddData(fs.CreateKey(), null, false);
|
||||
|
||||
_trackCounter = allFollowedStreams
|
||||
.GroupBy(x => new {Type = x.Type, Name = x.Username.ToLower()})
|
||||
.ToDictionary(
|
||||
x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||
x => x.Select(fs => fs.GuildId).ToHashSet());
|
||||
_trackCounter = allFollowedStreams.GroupBy(x => new { x.Type, Name = x.Username.ToLower() })
|
||||
.ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||
x => x.Select(fs => fs.GuildId).ToHashSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,43 +108,45 @@ public sealed class StreamNotificationService : INService
|
||||
_streamTracker.OnStreamsOnline += OnStreamsOnline;
|
||||
_ = _streamTracker.RunAsync();
|
||||
_notifCleanupTimer = new(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorLimit = TimeSpan.FromHours(12);
|
||||
var failingStreams = _streamTracker.GetFailingStreams(errorLimit, true)
|
||||
.ToList();
|
||||
|
||||
if (!failingStreams.Any())
|
||||
return;
|
||||
|
||||
var deleteGroups = failingStreams.GroupBy(x => x.Type)
|
||||
.ToDictionary(x => x.Key, x => x.Select(x => x.Name).ToList());
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
foreach (var kvp in deleteGroups)
|
||||
try
|
||||
{
|
||||
Log.Information($"Deleting {kvp.Value.Count} {kvp.Key} streams because " +
|
||||
$"they've been erroring for more than {errorLimit}: {string.Join(", ", kvp.Value)}");
|
||||
var errorLimit = TimeSpan.FromHours(12);
|
||||
var failingStreams = _streamTracker.GetFailingStreams(errorLimit, true).ToList();
|
||||
|
||||
var toDelete = uow.Set<FollowedStream>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username))
|
||||
.ToList();
|
||||
if (!failingStreams.Any())
|
||||
return;
|
||||
|
||||
uow.RemoveRange(toDelete);
|
||||
uow.SaveChanges();
|
||||
|
||||
foreach(var loginToDelete in kvp.Value)
|
||||
_streamTracker.UntrackStreamByKey(new(kvp.Key, loginToDelete));
|
||||
var deleteGroups = failingStreams.GroupBy(x => x.Type)
|
||||
.ToDictionary(x => x.Key, x => x.Select(x => x.Name).ToList());
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
foreach (var kvp in deleteGroups)
|
||||
{
|
||||
Log.Information($"Deleting {kvp.Value.Count} {kvp.Key} streams because "
|
||||
+ $"they've been erroring for more than {errorLimit}: {string.Join(", ", kvp.Value)}");
|
||||
|
||||
var toDelete = uow.Set<FollowedStream>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username))
|
||||
.ToList();
|
||||
|
||||
uow.RemoveRange(toDelete);
|
||||
uow.SaveChanges();
|
||||
|
||||
foreach (var loginToDelete in kvp.Value)
|
||||
_streamTracker.UntrackStreamByKey(new(kvp.Key, loginToDelete));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Error cleaning up FollowedStreams");
|
||||
Log.Error(ex.ToString());
|
||||
}
|
||||
}, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Error cleaning up FollowedStreams");
|
||||
Log.Error(ex.ToString());
|
||||
}
|
||||
},
|
||||
null,
|
||||
TimeSpan.FromMinutes(30),
|
||||
TimeSpan.FromMinutes(30));
|
||||
|
||||
_pubSub.Sub(_streamFollowKey, HandleFollowStream);
|
||||
_pubSub.Sub(_streamUnfollowKey, HandleUnfollowStream);
|
||||
@@ -164,36 +157,29 @@ public sealed class StreamNotificationService : INService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles follow stream pubs to keep the counter up to date.
|
||||
/// When counter reaches 0, stream is removed from tracking because
|
||||
/// that means no guilds are subscribed to that stream anymore
|
||||
/// Handles follow stream pubs to keep the counter up to date.
|
||||
/// When counter reaches 0, stream is removed from tracking because
|
||||
/// that means no guilds are subscribed to that stream anymore
|
||||
/// </summary>
|
||||
private ValueTask HandleFollowStream(FollowStreamPubData info)
|
||||
{
|
||||
_streamTracker.CacheAddData(info.Key, null, replace: false);
|
||||
_streamTracker.CacheAddData(info.Key, null, false);
|
||||
lock (_shardLock)
|
||||
{
|
||||
var key = info.Key;
|
||||
if (_trackCounter.ContainsKey(key))
|
||||
{
|
||||
_trackCounter[key].Add(info.GuildId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_trackCounter[key] = new()
|
||||
{
|
||||
info.GuildId
|
||||
};
|
||||
}
|
||||
_trackCounter[key] = new() { info.GuildId };
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles unfollow pubs to keep the counter up to date.
|
||||
/// When counter reaches 0, stream is removed from tracking because
|
||||
/// that means no guilds are subscribed to that stream anymore
|
||||
/// Handles unfollow pubs to keep the counter up to date.
|
||||
/// When counter reaches 0, stream is removed from tracking because
|
||||
/// that means no guilds are subscribed to that stream anymore
|
||||
/// </summary>
|
||||
private ValueTask HandleUnfollowStream(FollowStreamPubData info)
|
||||
{
|
||||
@@ -226,16 +212,14 @@ public sealed class StreamNotificationService : INService
|
||||
{
|
||||
var key = stream.CreateKey();
|
||||
if (_shardTrackedStreams.TryGetValue(key, out var fss))
|
||||
{
|
||||
await fss
|
||||
// send offline stream notifications only to guilds which enable it with .stoff
|
||||
.SelectMany(x => x.Value)
|
||||
.Where(x => _offlineNotificationServers.Contains(x.GuildId))
|
||||
.Select(fs => _client.GetGuild(fs.GuildId)
|
||||
?.GetTextChannel(fs.ChannelId)
|
||||
?.EmbedAsync(GetEmbed(fs.GuildId, stream)))
|
||||
.WhenAll();
|
||||
}
|
||||
// send offline stream notifications only to guilds which enable it with .stoff
|
||||
.SelectMany(x => x.Value)
|
||||
.Where(x => _offlineNotificationServers.Contains(x.GuildId))
|
||||
.Select(fs => _client.GetGuild(fs.GuildId)
|
||||
?.GetTextChannel(fs.ChannelId)
|
||||
?.EmbedAsync(GetEmbed(fs.GuildId, stream)))
|
||||
.WhenAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,35 +229,29 @@ public sealed class StreamNotificationService : INService
|
||||
{
|
||||
var key = stream.CreateKey();
|
||||
if (_shardTrackedStreams.TryGetValue(key, out var fss))
|
||||
{
|
||||
await fss
|
||||
.SelectMany(x => x.Value)
|
||||
.Select(fs =>
|
||||
{
|
||||
var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId);
|
||||
|
||||
if (textChannel is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var rep = new ReplacementBuilder()
|
||||
.WithOverride("%user%", () => fs.Username)
|
||||
.WithOverride("%platform%", () => fs.Type.ToString())
|
||||
.Build();
|
||||
|
||||
var message = string.IsNullOrWhiteSpace(fs.Message)
|
||||
? ""
|
||||
: rep.Replace(fs.Message);
|
||||
await fss.SelectMany(x => x.Value)
|
||||
.Select(fs =>
|
||||
{
|
||||
var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId);
|
||||
|
||||
return textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
|
||||
})
|
||||
.WhenAll();
|
||||
}
|
||||
if (textChannel is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username)
|
||||
.WithOverride("%platform%", () => fs.Type.ToString())
|
||||
.Build();
|
||||
|
||||
var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message);
|
||||
|
||||
return textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
|
||||
})
|
||||
.WhenAll();
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnStreamsOnline(List<StreamData> data)
|
||||
=> _pubSub.Pub(_streamsOnlineKey, data);
|
||||
|
||||
|
||||
private Task OnStreamsOffline(List<StreamData> data)
|
||||
=> _pubSub.Pub(_streamsOfflineKey, data);
|
||||
|
||||
@@ -281,14 +259,13 @@ public sealed class StreamNotificationService : INService
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigs
|
||||
.AsQueryable()
|
||||
.Include(x => x.FollowedStreams)
|
||||
.FirstOrDefault(x => x.GuildId == guildConfig.GuildId);
|
||||
var gc = uow.GuildConfigs.AsQueryable()
|
||||
.Include(x => x.FollowedStreams)
|
||||
.FirstOrDefault(x => x.GuildId == guildConfig.GuildId);
|
||||
|
||||
if (gc is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
|
||||
if (gc.NotifyStreamOffline)
|
||||
_offlineNotificationServers.Add(gc.GuildId);
|
||||
|
||||
@@ -332,7 +309,7 @@ public sealed class StreamNotificationService : INService
|
||||
|
||||
foreach (var s in gc.FollowedStreams)
|
||||
await PublishUnfollowStream(s);
|
||||
|
||||
|
||||
uow.SaveChanges();
|
||||
|
||||
return gc.FollowedStreams.Count;
|
||||
@@ -344,10 +321,10 @@ public sealed class StreamNotificationService : INService
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var fss = uow.Set<FollowedStream>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
// out of range
|
||||
if (fss.Count <= index)
|
||||
@@ -392,17 +369,11 @@ public sealed class StreamNotificationService : INService
|
||||
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams));
|
||||
|
||||
// add it to the database
|
||||
fs = new()
|
||||
{
|
||||
Type = data.StreamType,
|
||||
Username = data.UniqueName,
|
||||
ChannelId = channelId,
|
||||
GuildId = guildId,
|
||||
};
|
||||
fs = new() { Type = data.StreamType, Username = data.UniqueName, ChannelId = channelId, GuildId = guildId };
|
||||
|
||||
if (gc.FollowedStreams.Count >= 10)
|
||||
return null;
|
||||
|
||||
|
||||
gc.FollowedStreams.Add(fs);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
@@ -425,17 +396,17 @@ public sealed class StreamNotificationService : INService
|
||||
public IEmbedBuilder GetEmbed(ulong guildId, StreamData status)
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(status.Name)
|
||||
.WithUrl(status.StreamUrl)
|
||||
.WithDescription(status.StreamUrl)
|
||||
.AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true)
|
||||
.AddField(GetText(guildId, strs.viewers), status.IsLive ? status.Viewers.ToString() : "-", true);
|
||||
.WithTitle(status.Name)
|
||||
.WithUrl(status.StreamUrl)
|
||||
.WithDescription(status.StreamUrl)
|
||||
.AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true)
|
||||
.AddField(GetText(guildId, strs.viewers), status.IsLive ? status.Viewers.ToString() : "-", true);
|
||||
|
||||
if (status.IsLive)
|
||||
embed = embed.WithOkColor();
|
||||
else
|
||||
embed = embed.WithErrorColor();
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status.Title))
|
||||
embed.WithAuthor(status.Title);
|
||||
|
||||
@@ -463,13 +434,9 @@ public sealed class StreamNotificationService : INService
|
||||
uow.SaveChanges();
|
||||
|
||||
if (newValue)
|
||||
{
|
||||
_offlineNotificationServers.Add(guildId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_offlineNotificationServers.TryRemove(guildId);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
@@ -482,32 +449,22 @@ public sealed class StreamNotificationService : INService
|
||||
if (_shardTrackedStreams.TryGetValue(key, out var map))
|
||||
{
|
||||
if (map.TryGetValue(guildId, out var set))
|
||||
{
|
||||
return set;
|
||||
}
|
||||
else
|
||||
{
|
||||
return map[guildId] = new();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_shardTrackedStreams[key] = new()
|
||||
{
|
||||
{guildId, new()}
|
||||
};
|
||||
return _shardTrackedStreams[key][guildId];
|
||||
return map[guildId] = new();
|
||||
}
|
||||
|
||||
_shardTrackedStreams[key] = new() { { guildId, new() } };
|
||||
return _shardTrackedStreams[key][guildId];
|
||||
}
|
||||
|
||||
public bool SetStreamMessage(ulong guildId, int index, string message, out FollowedStream fs)
|
||||
public bool SetStreamMessage(
|
||||
ulong guildId,
|
||||
int index,
|
||||
string message,
|
||||
out FollowedStream fs)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var fss = uow.Set<FollowedStream>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
var fss = uow.Set<FollowedStream>().AsQueryable().Where(x => x.GuildId == guildId).OrderBy(x => x.Id).ToList();
|
||||
|
||||
if (fss.Count <= index)
|
||||
{
|
||||
@@ -536,8 +493,7 @@ public sealed class StreamNotificationService : INService
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
|
||||
var all = uow.Set<FollowedStream>()
|
||||
.ToList();
|
||||
var all = uow.Set<FollowedStream>().ToList();
|
||||
|
||||
if (all.Count == 0)
|
||||
return 0;
|
||||
@@ -554,4 +510,4 @@ public sealed class StreamNotificationService : INService
|
||||
public StreamDataKey Key { get; init; }
|
||||
public ulong GuildId { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
#nullable disable
|
||||
using System.Net;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using System.Net;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
@@ -17,7 +17,8 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
private readonly ConcurrentDictionary<ulong, bool> _atcs = new();
|
||||
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, (string From, string To)>> _users = new();
|
||||
|
||||
public TranslateService(IGoogleApiService google,
|
||||
public TranslateService(
|
||||
IGoogleApiService google,
|
||||
DbService db,
|
||||
IEmbedBuilderService eb,
|
||||
Bot bot)
|
||||
@@ -33,51 +34,48 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
var ctx = _db.GetDbContext();
|
||||
|
||||
var guilds = _bot.AllGuildConfigs.Select(x => x.GuildId).ToList();
|
||||
var cs = await ctx.AutoTranslateChannels
|
||||
.Include(x => x.Users)
|
||||
.Where(x => guilds.Contains(x.GuildId))
|
||||
.ToListAsyncEF();
|
||||
var cs = await ctx.AutoTranslateChannels.Include(x => x.Users)
|
||||
.Where(x => guilds.Contains(x.GuildId))
|
||||
.ToListAsyncEF();
|
||||
|
||||
foreach (var c in cs)
|
||||
{
|
||||
_atcs[c.ChannelId] = c.AutoDelete;
|
||||
_users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
|
||||
_users[c.ChannelId] =
|
||||
new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task LateExecute(IGuild guild, IUserMessage msg)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msg.Content))
|
||||
return;
|
||||
|
||||
|
||||
if (msg is { Channel: ITextChannel tch } um)
|
||||
{
|
||||
if (!_atcs.TryGetValue(tch.Id, out var autoDelete))
|
||||
return;
|
||||
|
||||
if (!_users.TryGetValue(tch.Id, out var users)
|
||||
|| !users.TryGetValue(um.Author.Id, out var langs))
|
||||
if (!_users.TryGetValue(tch.Id, out var users) || !users.TryGetValue(um.Author.Id, out var langs))
|
||||
return;
|
||||
|
||||
var output = await _google.Translate(msg.Content, langs.From, langs.To);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output)
|
||||
if (string.IsNullOrWhiteSpace(output)
|
||||
|| msg.Content.Equals(output, StringComparison.InvariantCultureIgnoreCase))
|
||||
return;
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor();
|
||||
|
||||
var embed = _eb.Create().WithOkColor();
|
||||
|
||||
if (autoDelete)
|
||||
{
|
||||
embed
|
||||
.WithAuthor(um.Author.ToString(), um.Author.GetAvatarUrl())
|
||||
.AddField(langs.From, um.Content)
|
||||
.AddField(langs.To, output);
|
||||
embed.WithAuthor(um.Author.ToString(), um.Author.GetAvatarUrl())
|
||||
.AddField(langs.From, um.Content)
|
||||
.AddField(langs.To, output);
|
||||
|
||||
await tch.EmbedAsync(embed);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await um.DeleteAsync();
|
||||
@@ -90,10 +88,7 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
return;
|
||||
}
|
||||
|
||||
await um.ReplyAsync(embed: embed
|
||||
.AddField(langs.To, output)
|
||||
.Build(),
|
||||
allowedMentions: AllowedMentions.None);
|
||||
await um.ReplyAsync(embed: embed.AddField(langs.To, output).Build(), allowedMentions: AllowedMentions.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,25 +105,18 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
{
|
||||
var ctx = _db.GetDbContext();
|
||||
|
||||
var old = await ctx.AutoTranslateChannels
|
||||
.ToLinqToDBTable()
|
||||
.FirstOrDefaultAsyncLinqToDB(x => x.ChannelId == channelId);
|
||||
|
||||
var old = await ctx.AutoTranslateChannels.ToLinqToDBTable()
|
||||
.FirstOrDefaultAsyncLinqToDB(x => x.ChannelId == channelId);
|
||||
|
||||
if (old is null)
|
||||
{
|
||||
ctx.AutoTranslateChannels
|
||||
.Add(new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
ChannelId = channelId,
|
||||
AutoDelete = autoDelete,
|
||||
});
|
||||
|
||||
ctx.AutoTranslateChannels.Add(new() { GuildId = guildId, ChannelId = channelId, AutoDelete = autoDelete });
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
_atcs[channelId] = autoDelete;
|
||||
_users[channelId] = new();
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -142,10 +130,8 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
return true;
|
||||
}
|
||||
|
||||
await ctx.AutoTranslateChannels
|
||||
.ToLinqToDBTable()
|
||||
.DeleteAsync(x => x.ChannelId == channelId);
|
||||
|
||||
await ctx.AutoTranslateChannels.ToLinqToDBTable().DeleteAsync(x => x.ChannelId == channelId);
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
_atcs.TryRemove(channelId, out _);
|
||||
_users.TryRemove(channelId, out _);
|
||||
@@ -154,53 +140,54 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
}
|
||||
|
||||
|
||||
private void UpdateUser(ulong channelId, ulong userId, string from, string to)
|
||||
private void UpdateUser(
|
||||
ulong channelId,
|
||||
ulong userId,
|
||||
string from,
|
||||
string to)
|
||||
{
|
||||
var dict = _users.GetOrAdd(channelId, new ConcurrentDictionary<ulong, (string, string)>());
|
||||
dict[userId] = (from, to);
|
||||
}
|
||||
|
||||
public async Task<bool?> RegisterUserAsync(ulong userId, ulong channelId, string from, string to)
|
||||
|
||||
public async Task<bool?> RegisterUserAsync(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
string from,
|
||||
string to)
|
||||
{
|
||||
if (!_google.Languages.ContainsKey(from) || !_google.Languages.ContainsKey(to))
|
||||
return null;
|
||||
|
||||
var ctx = _db.GetDbContext();
|
||||
var ch = await ctx.AutoTranslateChannels
|
||||
.GetByChannelId(channelId);
|
||||
|
||||
var ch = await ctx.AutoTranslateChannels.GetByChannelId(channelId);
|
||||
|
||||
if (ch is null)
|
||||
return null;
|
||||
|
||||
var user = ch.Users
|
||||
.FirstOrDefault(x => x.UserId == userId);
|
||||
var user = ch.Users.FirstOrDefault(x => x.UserId == userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
ch.Users.Add(user = new()
|
||||
{
|
||||
Source = from,
|
||||
Target = to,
|
||||
UserId = userId,
|
||||
});
|
||||
|
||||
ch.Users.Add(user = new() { Source = from, Target = to, UserId = userId });
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
UpdateUser(channelId, userId, from, to);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// if it's different from old settings, update
|
||||
if (user.Source != from || user.Target != to)
|
||||
{
|
||||
user.Source = from;
|
||||
user.Target = to;
|
||||
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
UpdateUser(channelId, userId, from, to);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -210,17 +197,16 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
|
||||
public async Task<bool> UnregisterUser(ulong channelId, ulong userId)
|
||||
{
|
||||
var ctx = _db.GetDbContext();
|
||||
var rows = await ctx.AutoTranslateUsers
|
||||
.ToLinqToDBTable()
|
||||
.DeleteAsync(x => x.UserId == userId &&
|
||||
x.Channel.ChannelId == channelId);
|
||||
var rows = await ctx.AutoTranslateUsers.ToLinqToDBTable()
|
||||
.DeleteAsync(x => x.UserId == userId && x.Channel.ChannelId == channelId);
|
||||
|
||||
if (_users.TryGetValue(channelId, out var inner))
|
||||
inner.TryRemove(userId, out _);
|
||||
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetLanguages() => _google.Languages.Select(x => x.Key);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetLanguages()
|
||||
=> _google.Languages.Select(x => x.Key);
|
||||
}
|
@@ -132,4 +132,4 @@ namespace NadekoBot.Modules.Searches.Services;
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -1,8 +1,8 @@
|
||||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
@@ -19,7 +19,8 @@ public partial class Searches
|
||||
// private static readonly Regex picartoRegex = new Regex(@"picarto.tv/(?<name>.+[^/])/?",
|
||||
// RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task StreamAdd(string link)
|
||||
@@ -35,7 +36,8 @@ public partial class Searches
|
||||
await ctx.Channel.EmbedAsync(embed, GetText(strs.stream_tracked));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(1)]
|
||||
@@ -43,7 +45,7 @@ public partial class Searches
|
||||
{
|
||||
if (--index < 0)
|
||||
return;
|
||||
|
||||
|
||||
var fs = await _service.UnfollowStreamAsync(ctx.Guild.Id, index);
|
||||
if (fs is null)
|
||||
{
|
||||
@@ -51,13 +53,11 @@ public partial class Searches
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync(
|
||||
strs.stream_removed(
|
||||
Format.Bold(fs.Username),
|
||||
fs.Type));
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_removed(Format.Bold(fs.Username), fs.Type));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task StreamsClear()
|
||||
@@ -66,107 +66,89 @@ public partial class Searches
|
||||
await ReplyConfirmLocalizedAsync(strs.streams_cleared);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task StreamList(int page = 1)
|
||||
{
|
||||
if (page-- < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (page-- < 1) return;
|
||||
|
||||
var streams = new List<FollowedStream>();
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var all = uow
|
||||
.GuildConfigsForId(ctx.Guild.Id, set => set.Include(gc => gc.FollowedStreams))
|
||||
.FollowedStreams
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
var all = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(gc => gc.FollowedStreams))
|
||||
.FollowedStreams.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
for (var index = all.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var fs = all[index];
|
||||
if (((SocketGuild) ctx.Guild).GetTextChannel(fs.ChannelId) is null)
|
||||
{
|
||||
if (((SocketGuild)ctx.Guild).GetTextChannel(fs.ChannelId) is null)
|
||||
await _service.UnfollowStreamAsync(fs.GuildId, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
streams.Insert(0, fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page, cur =>
|
||||
{
|
||||
var elements = streams.Skip(cur * 12).Take(12)
|
||||
.ToList();
|
||||
|
||||
if (elements.Count == 0)
|
||||
await ctx.SendPaginatedConfirmAsync(page,
|
||||
cur =>
|
||||
{
|
||||
return _eb.Create()
|
||||
.WithDescription(GetText(strs.streams_none))
|
||||
.WithErrorColor();
|
||||
}
|
||||
var elements = streams.Skip(cur * 12).Take(12).ToList();
|
||||
|
||||
var eb = _eb.Create()
|
||||
.WithTitle(GetText(strs.streams_follow_title))
|
||||
.WithOkColor();
|
||||
for (var index = 0; index < elements.Count; index++)
|
||||
{
|
||||
var elem = elements[index];
|
||||
eb.AddField(
|
||||
$"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}",
|
||||
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
||||
true);
|
||||
}
|
||||
if (elements.Count == 0)
|
||||
return _eb.Create().WithDescription(GetText(strs.streams_none)).WithErrorColor();
|
||||
|
||||
return eb;
|
||||
}, streams.Count, 12);
|
||||
var eb = _eb.Create().WithTitle(GetText(strs.streams_follow_title)).WithOkColor();
|
||||
for (var index = 0; index < elements.Count; index++)
|
||||
{
|
||||
var elem = elements[index];
|
||||
eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}",
|
||||
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
},
|
||||
streams.Count,
|
||||
12);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task StreamOffline()
|
||||
{
|
||||
var newValue = _service.ToggleStreamOffline(ctx.Guild.Id);
|
||||
if (newValue)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_off_enabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_off_disabled);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task StreamMessage(int index, [Leftover] string message)
|
||||
{
|
||||
if (--index < 0)
|
||||
return;
|
||||
|
||||
|
||||
if (!_service.SetStreamMessage(ctx.Guild.Id, index, message, out var fs))
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_not_following);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_message_reset(Format.Bold(fs.Username)));
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_message_set(Format.Bold(fs.Username)));
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task StreamMessageAll([Leftover] string message)
|
||||
@@ -181,8 +163,9 @@ public partial class Searches
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.stream_message_set_all(count));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task StreamCheck(string url)
|
||||
{
|
||||
@@ -194,17 +177,12 @@ public partial class Searches
|
||||
await ReplyErrorLocalizedAsync(strs.no_channel_found);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (data.IsLive)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.streamer_online(
|
||||
Format.Bold(data.Name),
|
||||
await ReplyConfirmLocalizedAsync(strs.streamer_online(Format.Bold(data.Name),
|
||||
Format.Bold(data.Viewers.ToString())));
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.streamer_offline(data.Name));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -212,4 +190,4 @@ public partial class Searches
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,7 +6,14 @@ public partial class Searches
|
||||
[Group]
|
||||
public class TranslateCommands : NadekoSubmodule<ITranslateService>
|
||||
{
|
||||
[NadekoCommand, Aliases]
|
||||
public enum AutoDeleteAutoTranslate
|
||||
{
|
||||
Del,
|
||||
Nodel
|
||||
}
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
public async Task Translate(string from, string to, [Leftover] string text = null)
|
||||
{
|
||||
try
|
||||
@@ -14,10 +21,7 @@ public partial class Searches
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
var translation = await _service.Translate(from, to, text);
|
||||
|
||||
var embed = _eb.Create(ctx)
|
||||
.WithOkColor()
|
||||
.AddField(from, text, false)
|
||||
.AddField(to, translation, false);
|
||||
var embed = _eb.Create(ctx).WithOkColor().AddField(from, text).AddField(to, translation);
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
@@ -27,55 +31,44 @@ public partial class Searches
|
||||
}
|
||||
}
|
||||
|
||||
public enum AutoDeleteAutoTranslate
|
||||
{
|
||||
Del,
|
||||
Nodel
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(ChannelPerm.ManageMessages)]
|
||||
[OwnerOnly]
|
||||
public async Task AutoTranslate(AutoDeleteAutoTranslate autoDelete = AutoDeleteAutoTranslate.Nodel)
|
||||
{
|
||||
var toggle = await _service.ToggleAtl(ctx.Guild.Id, ctx.Channel.Id, autoDelete == AutoDeleteAutoTranslate.Del);
|
||||
var toggle =
|
||||
await _service.ToggleAtl(ctx.Guild.Id, ctx.Channel.Id, autoDelete == AutoDeleteAutoTranslate.Del);
|
||||
if (toggle)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.atl_started);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.atl_stopped);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task AutoTransLang()
|
||||
{
|
||||
if (await _service.UnregisterUser(ctx.Channel.Id, ctx.User.Id))
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.atl_removed);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task AutoTransLang(string from, string to)
|
||||
{
|
||||
var succ = await _service.RegisterUserAsync(ctx.User.Id,
|
||||
ctx.Channel.Id,
|
||||
from.ToLower(),
|
||||
to.ToLower());
|
||||
var succ = await _service.RegisterUserAsync(ctx.User.Id, ctx.Channel.Id, from.ToLower(), to.ToLower());
|
||||
|
||||
if (succ is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.atl_not_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (succ is false)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.invalid_lang);
|
||||
@@ -85,9 +78,10 @@ public partial class Searches
|
||||
await ReplyConfirmLocalizedAsync(strs.atl_set(from, to));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Translangs()
|
||||
=> await ctx.Channel.SendTableAsync(_service.GetLanguages(), str => $"{str,-15}", 3);
|
||||
=> await ctx.Channel.SendTableAsync(_service.GetLanguages(), str => $"{str,-15}");
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,7 +14,8 @@ public partial class Searches
|
||||
public XkcdCommands(IHttpClientFactory factory)
|
||||
=> _httpFactory = factory;
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[Priority(0)]
|
||||
public async Task Xkcd(string arg = null)
|
||||
{
|
||||
@@ -25,27 +26,31 @@ public partial class Searches
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var res = await http.GetStringAsync($"{_xkcdUrl}/info.0.json");
|
||||
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
|
||||
var embed = _eb.Create().WithOkColor()
|
||||
.WithImageUrl(comic.ImageLink)
|
||||
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{_xkcdUrl}/{comic.Num}")
|
||||
.AddField(GetText(strs.comic_number), comic.Num.ToString(), true)
|
||||
.AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true);
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithImageUrl(comic.ImageLink)
|
||||
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{_xkcdUrl}/{comic.Num}")
|
||||
.AddField(GetText(strs.comic_number), comic.Num.ToString(), true)
|
||||
.AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true);
|
||||
var sent = await ctx.Channel.EmbedAsync(embed);
|
||||
|
||||
await Task.Delay(10000);
|
||||
|
||||
await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt.ToString(), false).Build());
|
||||
await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt).Build());
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.comic_not_found);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Xkcd(new NadekoRandom().Next(1, 1750));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[NadekoCommand]
|
||||
[Aliases]
|
||||
[Priority(1)]
|
||||
public async Task Xkcd(int num)
|
||||
{
|
||||
@@ -58,17 +63,17 @@ public partial class Searches
|
||||
|
||||
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithImageUrl(comic.ImageLink)
|
||||
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{_xkcdUrl}/{num}")
|
||||
.AddField(GetText(strs.comic_number), comic.Num.ToString(), true)
|
||||
.AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true);
|
||||
|
||||
.WithOkColor()
|
||||
.WithImageUrl(comic.ImageLink)
|
||||
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{_xkcdUrl}/{num}")
|
||||
.AddField(GetText(strs.comic_number), comic.Num.ToString(), true)
|
||||
.AddField(GetText(strs.date), $"{comic.Month}/{comic.Year}", true);
|
||||
|
||||
var sent = await ctx.Channel.EmbedAsync(embed);
|
||||
|
||||
await Task.Delay(10000);
|
||||
|
||||
await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt.ToString(), false).Build());
|
||||
await sent.ModifyAsync(m => m.Embed = embed.AddField("Alt", comic.Alt).Build());
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
@@ -82,10 +87,13 @@ public partial class Searches
|
||||
public int Num { get; set; }
|
||||
public string Month { get; set; }
|
||||
public string Year { get; set; }
|
||||
|
||||
[JsonProperty("safe_title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("img")]
|
||||
public string ImageLink { get; set; }
|
||||
|
||||
public string Alt { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -51,4 +51,4 @@ public partial class Searches
|
||||
// //}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user